@echo off
setlocal EnableExtensions
chcp 65001 >nul
title Mirage API - Client Auto-Setup v2
color 0B
echo.
echo   ==============================================
echo          MIRAGE API  /  CLIENT AUTO-SETUP v2.1 BETA
echo   ==============================================
echo.
echo   Select one client; the setup checks the API key.
echo   Claude Code, Codex, Cline, Roo Code, Kilo Code,
echo   OpenCode, ZCode and detected editors.
echo   Existing files are backed up before changes.
echo.

set "MIRAGE_SETUP_FILE=%~f0"
where powershell.exe >nul 2>&1
if errorlevel 1 (
  echo PowerShell was not found.
  echo.
  pause
  exit /b 1
)

powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "$raw = [System.IO.File]::ReadAllText($env:MIRAGE_SETUP_FILE, [System.Text.Encoding]::UTF8); $marker = '# --- MIRAGE POWERSHELL PAYLOAD ---'; $index = $raw.LastIndexOf($marker); if ($index -lt 0) { Write-Error 'PowerShell payload missing'; exit 1 }; $payload = $raw.Substring($index + $marker.Length); & ([scriptblock]::Create($payload))"

set "MIRAGE_EXIT_CODE=%ERRORLEVEL%"
echo.
if "%MIRAGE_EXIT_CODE%"=="0" (
  echo Setup completed.
) else if "%MIRAGE_EXIT_CODE%"=="2" (
  echo Setup was not completed. See details above.
) else if "%MIRAGE_EXIT_CODE%"=="3" (
  echo Setup was not completed. See details above.
) else if "%MIRAGE_EXIT_CODE%"=="4" (
  echo Setup was not completed. See details above.
) else if "%MIRAGE_EXIT_CODE%"=="5" (
  echo Setup was not completed. See details above.
) else if "%MIRAGE_EXIT_CODE%"=="6" (
  echo Setup was not completed. See details above.
) else if "%MIRAGE_EXIT_CODE%"=="7" (
  echo Setup was not completed. See details above.
) else if "%MIRAGE_EXIT_CODE%"=="8" (
  echo Setup was not completed. See details above.
) else if "%MIRAGE_EXIT_CODE%"=="9" (
  echo Setup was not completed. See details above.
) else if not "%MIRAGE_EXIT_CODE%"=="0" (
  echo Setup was not completed. See details above.
)
echo.
pause
exit /b %MIRAGE_EXIT_CODE%

# --- MIRAGE POWERSHELL PAYLOAD ---

$ErrorActionPreference = 'Stop'
$ProgressPreference    = 'SilentlyContinue'

try {
    [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
    [Console]::InputEncoding  = [System.Text.Encoding]::UTF8
} catch { }
$OutputEncoding = [System.Text.Encoding]::UTF8

$ScriptVersion = '2.1.0'

# Exit codes
$EXIT_OK            = 0
$EXIT_FATAL         = 1
$EXIT_NO_KEY        = 2
$EXIT_AUTH          = 3
$EXIT_NETWORK       = 4
$EXIT_PARTIAL       = 5
$EXIT_OLD_PS        = 6
$EXIT_CANCELLED     = 7
$EXIT_INVALID_TOKEN = 8
$EXIT_BAD_MODEL     = 9

if ($PSVersionTable.PSVersion -lt [version]'5.1') {
    Write-Host 'PowerShell 5.1 or newer is required. Update Windows Management Framework / PowerShell.' -ForegroundColor Red
    exit $EXIT_OLD_PS
}

try {
    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
} catch { }

# ---------------------------------------------------------------------------
# Gateway endpoints
# ---------------------------------------------------------------------------
$GatewayHost       = 'mirage-api.xyz'
$AnthropicBaseUrl  = 'https://mirage-api.xyz'
$BaseUrl           = 'https://mirage-api.xyz/v1'
$ChatUrl           = 'https://mirage-api.xyz/v1/chat/completions'
$ResponsesUrl      = 'https://mirage-api.xyz/v1/responses'
$ModelsUrl         = 'https://mirage-api.xyz/v1/models'
$ProviderLabel     = 'Mirage API'
$ProviderId        = 'mirage'
$EnvKeyName        = 'MIRAGE_API_KEY'

# ---------------------------------------------------------------------------
# Model catalog - 16 models
# ---------------------------------------------------------------------------
$ModelCatalog = @(
    [pscustomobject]@{ Index = 1;  Id = 'gpt-6-astra';       Name = 'GPT-6 Astra';       Vendor = 'OpenAI';       CtxLabel = '1M';  Context = 1000000;  MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 2;  Id = 'gpt-5.6-sol';       Name = 'GPT-5.6 Sol';       Vendor = 'OpenAI';       CtxLabel = '1M';  Context = 1000000;  MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 3;  Id = 'gpt-6.1-sol';       Name = 'GPT-6.1 Sol';       Vendor = 'OpenAI';       CtxLabel = '1M';  Context = 1000000;  MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 4;  Id = 'claude-opus-5.5';   Name = 'Claude Opus 5.5';   Vendor = 'Anthropic';    CtxLabel = '1M';  Context = 1000000;  MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 5;  Id = 'claude-opus-5';     Name = 'Claude Opus 5';     Vendor = 'Anthropic';    CtxLabel = '1M';  Context = 1000000;  MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 6;  Id = 'claude-fable-5.1';  Name = 'Claude Fable 5.1';  Vendor = 'Anthropic';    CtxLabel = '1M';  Context = 1000000;  MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 7;  Id = 'gemini-3.8-flash';  Name = 'Gemini 3.8 Flash';  Vendor = 'Google';       CtxLabel = '1M';    Context = 1000000; MaxOut = 65536; Vision = $true  }
    [pscustomobject]@{ Index = 8;  Id = 'gemini-3.1-pro';    Name = 'Gemini 3.1 Pro';    Vendor = 'Google';       CtxLabel = '1M';    Context = 1000000; MaxOut = 65536; Vision = $true  }
    [pscustomobject]@{ Index = 9;  Id = 'gemini-4-argon';    Name = 'Gemini 4 Argon';    Vendor = 'Google';       CtxLabel = '1M';    Context = 1000000; MaxOut = 65536; Vision = $true  }
    [pscustomobject]@{ Index = 10; Id = 'claude-sonnet-5.5'; Name = 'Claude Sonnet 5.5'; Vendor = 'Anthropic';    CtxLabel = '1M';    Context = 1000000; MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 11; Id = 'claude-opus-4.8';   Name = 'Claude Opus 4.8';   Vendor = 'Anthropic';    CtxLabel = '1M';    Context = 1000000; MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 12; Id = 'claude-haiku-5.5';  Name = 'Claude Haiku 5.5';  Vendor = 'Anthropic';    CtxLabel = '1M';    Context = 1000000; MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 13; Id = 'gpt-6-sol';         Name = 'GPT-6 Sol';         Vendor = 'OpenAI';       CtxLabel = '1M'; Context = 1000000; MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 14; Id = 'gpt-6-luna';        Name = 'GPT-6 Luna';        Vendor = 'OpenAI';       CtxLabel = '1M'; Context = 1000000; MaxOut = 32768; Vision = $true  }
    [pscustomobject]@{ Index = 15; Id = 'kimi-k3';           Name = 'Kimi K3';           Vendor = 'Moonshot AI';  CtxLabel = '1M';    Context = 1000000; MaxOut = 32768; Vision = $false }
    [pscustomobject]@{ Index = 16; Id = 'qwen3.8-max';       Name = 'Qwen3.8-Max';       Vendor = 'Alibaba Qwen'; CtxLabel = '1M';    Context = 1000000; MaxOut = 32768; Vision = $true  }
)

$failedSteps  = New-Object 'System.Collections.Generic.List[string]'
$okSteps      = New-Object 'System.Collections.Generic.List[string]'
$skippedSteps = New-Object 'System.Collections.Generic.List[string]'
$logLines     = New-Object 'System.Collections.Generic.List[string]'

# ---------------------------------------------------------------------------
# Small helpers
# ---------------------------------------------------------------------------
function Get-UserHomeDirectory {
    if (-not [string]::IsNullOrWhiteSpace($env:USERPROFILE)) { return $env:USERPROFILE }
    if (-not [string]::IsNullOrWhiteSpace($HOME)) { return $HOME }
    return [Environment]::GetFolderPath('UserProfile')
}

function Get-AppDataDirectory {
    if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) { return $env:APPDATA }
    return [Environment]::GetFolderPath('ApplicationData')
}

function Get-ConfigRootDirectory {
    if (-not [string]::IsNullOrWhiteSpace($env:XDG_CONFIG_HOME)) { return $env:XDG_CONFIG_HOME }
    return (Join-Path (Get-UserHomeDirectory) '.config')
}

function Write-Section([string]$Message) {
    Write-Host "`n$Message" -ForegroundColor Cyan
}

function Invoke-ConfigStep([string]$Name, [scriptblock]$Action) {
    try {
        & $Action
        [void]$okSteps.Add($Name)
        [void]$logLines.Add("OK       - $Name")
        Write-Host "  [OK] $Name" -ForegroundColor Green
    } catch {
        [void]$failedSteps.Add($Name)
        [void]$logLines.Add("FAILED   - $Name - $($_.Exception.Message)")
        Write-Host "  [ERROR] $Name - $($_.Exception.Message)" -ForegroundColor Red
    }
}

function Skip-Step([string]$Name) {
    [void]$skippedSteps.Add($Name)
    [void]$logLines.Add("SKIPPED  - $Name")
    Write-Host "  [SKIPPED] $Name" -ForegroundColor DarkGray
}

function Backup-File([string]$Path) {
    if (Test-Path -LiteralPath $Path -PathType Leaf) {
        $backupPath = "$Path.bak.$([DateTime]::UtcNow.ToString('yyyyMMdd-HHmmss-fff'))-$([Guid]::NewGuid().ToString('N').Substring(0,8))"
        Copy-Item -LiteralPath $Path -Destination $backupPath -ErrorAction Stop
        Write-Host "    Backup: $backupPath" -ForegroundColor DarkGray
    }
}

function Ensure-ParentDirectory([string]$Path) {
    $parent = Split-Path -Parent $Path
    if (-not [string]::IsNullOrEmpty($parent) -and -not (Test-Path -LiteralPath $parent -PathType Container)) {
        New-Item -ItemType Directory -Path $parent -Force -ErrorAction Stop | Out-Null
    }
}

function Write-AtomicText([string]$Path, [string]$Text) {
    Ensure-ParentDirectory $Path
    $temporaryPath = "$Path.mirage-tmp"
    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    try {
        [System.IO.File]::WriteAllText($temporaryPath, $Text, $utf8NoBom)
        Move-Item -LiteralPath $temporaryPath -Destination $Path -Force -ErrorAction Stop
    }
    finally {
        if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) {
            Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
        }
    }
    [void]$logLines.Add("WROTE    - $Path")
}

# Windows PowerShell 5.1 escapes non-ASCII as \uXXXX. Turn those back into real
# characters so Cyrillic paths / values stay readable. Valid JSON either way.
function Expand-JsonUnicodeEscapes([string]$Text) {
    if ([string]::IsNullOrEmpty($Text)) { return $Text }
    $builder = New-Object System.Text.StringBuilder
    $length  = $Text.Length
    $i = 0
    while ($i -lt $length) {
        $ch = $Text[$i]
        if ($ch -eq '\') {
            if (($i + 1) -lt $length -and $Text[$i + 1] -eq '\') {
                [void]$builder.Append('\\')
                $i += 2
                continue
            }
            if (($i + 1) -lt $length -and $Text[$i + 1] -eq 'u') {
                $units = New-Object 'System.Collections.Generic.List[int]'
                $j = $i
                while (($j + 5) -lt $length -and $Text[$j] -eq '\' -and $Text[$j + 1] -eq 'u') {
                    $hex  = $Text.Substring($j + 2, 4)
                    $code = 0
                    if (-not [int]::TryParse($hex, [System.Globalization.NumberStyles]::HexNumber, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$code)) { break }
                    [void]$units.Add($code)
                    $j += 6
                }
                if ($units.Count -gt 0) {
                    $allNonAscii = $true
                    foreach ($unit in $units) { if ($unit -lt 0x80) { $allNonAscii = $false; break } }
                    if ($allNonAscii) {
                        $chars = New-Object 'System.Collections.Generic.List[char]'
                        $k = 0
                        while ($k -lt $units.Count) {
                            $unit = $units[$k]
                            if ($unit -ge 0xD800 -and $unit -le 0xDBFF -and ($k + 1) -lt $units.Count -and $units[$k + 1] -ge 0xDC00 -and $units[$k + 1] -le 0xDFFF) {
                                $combined = 0x10000 + (($unit - 0xD800) * 0x400) + ($units[$k + 1] - 0xDC00)
                                foreach ($c in [char]::ConvertFromUtf32($combined).ToCharArray()) { [void]$chars.Add($c) }
                                $k += 2
                            } else {
                                [void]$chars.Add([char]$unit)
                                $k += 1
                            }
                        }
                        [void]$builder.Append([string]::new($chars.ToArray()))
                        $i = $j
                        continue
                    }
                }
            }
        }
        [void]$builder.Append($ch)
        $i += 1
    }
    return $builder.ToString()
}

function ConvertTo-JsonText($InputObject) {
    $json = ConvertTo-Json -InputObject $InputObject -Depth 100
    return (Expand-JsonUnicodeEscapes $json)
}

function Publish-EnvironmentChange {
    try {
        if (-not ([System.Management.Automation.PSTypeName]'Mirage.Native').Type) {
            $nativeCode = @'
using System;
using System.Runtime.InteropServices;

namespace Mirage {
    public static class Native {
        [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        public static extern IntPtr SendMessageTimeout(
            IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam,
            uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);
    }
}
'@
            Add-Type -TypeDefinition $nativeCode -ErrorAction Stop
        }
        $hwndBroadcast   = [IntPtr]0xffff
        $wmSettingChange = [uint32]0x001A
        $smtoAbortIfHung = [uint32]0x0002
        $timeoutMs       = [uint32]5000
        $queryResult     = [UIntPtr]::Zero
        [void][Mirage.Native]::SendMessageTimeout($hwndBroadcast, $wmSettingChange, [UIntPtr]::Zero, 'Environment', $smtoAbortIfHung, $timeoutMs, [ref]$queryResult)
    } catch { }
}

# ---------------------------------------------------------------------------
# JSON / JSONC toolkit
# ---------------------------------------------------------------------------
function Convert-JsoncToJson([string]$Text) {
    if ($Text.Length -gt 0 -and [int][char]$Text[0] -eq 0xFEFF) {
        $Text = $Text.Substring(1)
    }
    $commentFree = New-Object System.Text.StringBuilder
    $quote = [char]34; $slash = [char]47; $star = [char]42; $backslash = [char]92
    $lineFeed = [char]10; $carriageReturn = [char]13; $space = [char]32
    $insideString = $false; $escaped = $false; $lineComment = $false; $blockComment = $false

    for ($i = 0; $i -lt $Text.Length; $i++) {
        $ch = $Text[$i]
        if ($lineComment) {
            if ($ch -eq $lineFeed -or $ch -eq $carriageReturn) { $lineComment = $false; [void]$commentFree.Append($ch) } else { [void]$commentFree.Append($space) }
            continue
        }
        if ($blockComment) {
            if ($ch -eq $star -and ($i + 1) -lt $Text.Length -and $Text[$i + 1] -eq $slash) { [void]$commentFree.Append('  '); $i++; $blockComment = $false }
            elseif ($ch -eq $lineFeed -or $ch -eq $carriageReturn) { [void]$commentFree.Append($ch) } else { [void]$commentFree.Append($space) }
            continue
        }
        if ($insideString) {
            [void]$commentFree.Append($ch)
            if ($escaped) { $escaped = $false } elseif ($ch -eq $backslash) { $escaped = $true } elseif ($ch -eq $quote) { $insideString = $false }
            continue
        }
        if ($ch -eq $quote) { $insideString = $true; [void]$commentFree.Append($ch); continue }
        if ($ch -eq $slash -and ($i + 1) -lt $Text.Length) {
            $next = $Text[$i + 1]
            if ($next -eq $slash) { [void]$commentFree.Append('  '); $i++; $lineComment = $true; continue }
            if ($next -eq $star) { [void]$commentFree.Append('  '); $i++; $blockComment = $true; continue }
        }
        [void]$commentFree.Append($ch)
    }

    $source = $commentFree.ToString()
    $clean = New-Object System.Text.StringBuilder
    $insideString = $false; $escaped = $false
    for ($i = 0; $i -lt $source.Length; $i++) {
        $ch = $source[$i]
        if ($insideString) {
            [void]$clean.Append($ch)
            if ($escaped) { $escaped = $false } elseif ($ch -eq $backslash) { $escaped = $true } elseif ($ch -eq $quote) { $insideString = $false }
            continue
        }
        if ($ch -eq $quote) { $insideString = $true; [void]$clean.Append($ch); continue }
        if ($ch -eq ',') {
            $lookAhead = $i + 1
            while ($lookAhead -lt $source.Length -and [char]::IsWhiteSpace($source[$lookAhead])) { $lookAhead++ }
            if ($lookAhead -lt $source.Length -and ($source[$lookAhead] -eq '}' -or $source[$lookAhead] -eq ']')) { continue }
        }
        [void]$clean.Append($ch)
    }
    return $clean.ToString()
}

function Get-JsonValue($Object, [string]$Name) {
    if ($null -eq $Object) { return $null }
    if ($Object -is [System.Collections.IDictionary]) { if ($Object.Contains($Name)) { return ,$Object[$Name] }; return $null }
    $property = $Object.PSObject.Properties[$Name]
    if ($null -ne $property) { return ,$property.Value }
    return $null
}

function Set-JsonValue($Object, [string]$Name, $Value) {
    if ($Object -is [System.Collections.IDictionary]) { $Object[$Name] = $Value; return }
    $property = $Object.PSObject.Properties[$Name]
    if ($null -ne $property) { $property.Value = $Value }
    else { $Object | Add-Member -MemberType NoteProperty -Name $Name -Value $Value -Force }
}

function Remove-JsonValue($Object, [string]$Name) {
    if ($null -eq $Object) { return }
    if ($Object -is [System.Collections.IDictionary]) {
        if ($Object.Contains($Name)) { [void]$Object.Remove($Name) }
        return
    }
    $property = $Object.PSObject.Properties[$Name]
    if ($null -ne $property) { [void]$Object.PSObject.Properties.Remove($Name) }
}

function Assert-JsonObject($Object, [string]$Label) {
    if ($null -eq $Object -or $Object -is [Array] -or $Object -is [string] -or $Object -is [ValueType]) {
        throw "$Label must contain a JSON object. It was left untouched."
    }
}

function Update-JsonSettings([string]$Path, [scriptblock]$Mutator) {
    Ensure-ParentDirectory $Path
    $settings = [pscustomobject]@{}
    if (Test-Path -LiteralPath $Path -PathType Leaf) {
        $rawJson = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
        if (-not [string]::IsNullOrWhiteSpace($rawJson)) {
            $cleanedJson = Convert-JsoncToJson $rawJson
            if (-not [string]::IsNullOrWhiteSpace($cleanedJson)) {
                try {
                    $settings = ConvertFrom-Json -InputObject $cleanedJson -ErrorAction Stop
                    Assert-JsonObject $settings $Path
                } catch {
                    throw "Invalid JSON/JSONC in '$Path': $($_.Exception.Message)"
                }
            }
        }
        Backup-File $Path
    }
    & $Mutator $settings
    $jsonText = ConvertTo-JsonText $settings
    Write-AtomicText $Path $jsonText
}

function Get-OrCreate-JsonObject($Parent, [string]$Name) {
    $value = Get-JsonValue $Parent $Name
    if ($null -eq $value) { $value = [pscustomobject]@{}; Set-JsonValue $Parent $Name $value }
    Assert-JsonObject $value $Name
    return $value
}

# ---------------------------------------------------------------------------
# TOML toolkit (Codex)
# ---------------------------------------------------------------------------
function Quote-TomlString([string]$Value) {
    $escaped = $Value.Replace('\', '\\').Replace('"', '\"')
    return '"' + $escaped + '"'
}

function Set-TomlFields([string]$Text, [string]$Section, [System.Collections.IDictionary]$Fields) {
    if ($Text.Length -gt 0 -and [int][char]$Text[0] -eq 0xFEFF) {
        $Text = $Text.Substring(1)
    }
    $lines = New-Object 'System.Collections.Generic.List[string]'
    foreach ($line in [regex]::Split($Text, '\r?\n')) { [void]$lines.Add($line) }
    if ($lines.Count -eq 1 -and $lines[0] -eq '') { $lines.Clear() }

    if ([string]::IsNullOrEmpty($Section)) {
        $start = 0
        $end = $lines.Count
        for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^\s*\[') { $end = $i; break } }
    } else {
        $targetHeader = '^\s*\[' + [regex]::Escape($Section) + '\]\s*(?:#.*)?$'
        $start = -1
        for ($i = 0; $i -lt $lines.Count; $i++) { if ($lines[$i] -match $targetHeader) { $start = $i; break } }
        if ($start -lt 0) {
            if ($lines.Count -gt 0 -and $lines[$lines.Count - 1].Trim() -ne '') { [void]$lines.Add('') }
            [void]$lines.Add("[$Section]")
            $start = $lines.Count - 1
            $end = $lines.Count
        } else {
            $end = $lines.Count
            for ($i = $start + 1; $i -lt $lines.Count; $i++) { if ($lines[$i] -match '^\s*\[') { $end = $i; break } }
        }
    }

    foreach ($key in $Fields.Keys) {
        $keyPattern = '^\s*' + [regex]::Escape([string]$key) + '\s*='
        $foundIndex = -1
        for ($i = $start; $i -lt $end; $i++) {
            if ($lines[$i] -match $keyPattern) {
                if ($foundIndex -lt 0) { $foundIndex = $i } else { $lines.RemoveAt($i); $i--; $end-- }
            }
        }
        $newLine = [string]$key + ' = ' + [string]$Fields[$key]
        if ($foundIndex -ge 0) { $lines[$foundIndex] = $newLine } else { $lines.Insert($end, $newLine); $end++ }
    }

    while ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -eq '') { $lines.RemoveAt($lines.Count - 1) }
    $result = [string]::Join("`r`n", $lines)
    if (-not $result.EndsWith("`r`n")) { $result += "`r`n" }
    return $result
}

# ---------------------------------------------------------------------------
# Model selection
# ---------------------------------------------------------------------------
function Resolve-ModelChoice([string]$RawInput, [array]$Catalog) {
    if ([string]::IsNullOrWhiteSpace($RawInput)) {
        return $Catalog[0]
    }
    $trimmed = $RawInput.Trim()
    $parsedIndex = 0
    if ([int]::TryParse($trimmed, [ref]$parsedIndex)) {
        if ($parsedIndex -ge 1 -and $parsedIndex -le $Catalog.Count) {
            return $Catalog[$parsedIndex - 1]
        }
        return $null
    }
    foreach ($entry in $Catalog) {
        if ([string]::Equals($entry.Id, $trimmed, [System.StringComparison]::OrdinalIgnoreCase)) {
            return $entry
        }
    }
    if ($trimmed -match '^[A-Za-z0-9][A-Za-z0-9._:/-]{1,99}$') {
        return [pscustomobject]@{
            Index    = 0
            Id       = $trimmed
            Name     = $trimmed
            Vendor   = 'Custom'
            CtxLabel = '524K'
            Context  = 524288
            MaxOut   = 32768
            Vision   = $true
        }
    }
    return $null
}

function Get-EffectiveCatalog([array]$Catalog, $SelectedEntry) {
    $list = New-Object 'System.Collections.Generic.List[object]'
    $found = $false
    foreach ($item in $Catalog) {
        [void]$list.Add($item)
        if ([string]::Equals($item.Id, $SelectedEntry.Id, [System.StringComparison]::OrdinalIgnoreCase)) {
            $found = $true
        }
    }
    if (-not $found -and $null -ne $SelectedEntry) {
        [void]$list.Insert(0, $SelectedEntry)
    }
    return ,$list.ToArray()
}

# ---------------------------------------------------------------------------
# Editor process detection
# ---------------------------------------------------------------------------
$EditorProcessNames = @(
    'Code',
    'Code - Insiders',
    'Cursor',
    'Windsurf',
    'VSCodium',
    'ZCode',
    'Trae',
    'Kilo Code',
    'Kiro'
)

function Get-RunningEditorProcesses {
    return ,@(Get-Process -Name $EditorProcessNames -ErrorAction SilentlyContinue)
}

function Show-RunningEditorsWarning([array]$Processes) {
    Write-Host ''
    Write-Host 'WARNING: running editors detected:' -ForegroundColor Red
    $grouped = $Processes | Group-Object -Property ProcessName
    foreach ($grp in $grouped) {
        $pids = @($grp.Group | ForEach-Object { $_.Id })
        if ($pids.Count -eq 1) {
            Write-Host "  - $($grp.Name) (PID: $($pids[0]))" -ForegroundColor Yellow
        } else {
            $sampleCount = [Math]::Min($pids.Count, 5)
            $sampleText  = ($pids[0..($sampleCount - 1)] -join ', ')
            if ($pids.Count -gt 5) { $sampleText += ', ...' }
            Write-Host "  - $($grp.Name) (processes: $($pids.Count), PID: $sampleText)" -ForegroundColor Yellow
        }
    }
    Write-Host ''
    Write-Host 'Please close VS Code / Cursor / Windsurf / VSCodium / ZCode / Trae before running the installer.' -ForegroundColor Red
    Write-Host 'Otherwise the editor will overwrite settings.json and Cline state with its cached data on exit.' -ForegroundColor Red
    Write-Host ''
}

# ---------------------------------------------------------------------------
# Gateway probing
# ---------------------------------------------------------------------------
function Read-ResponseContent($Response) {
    if ($null -eq $Response) { return '' }
    $content = $null
    try { $content = $Response.Content } catch { }
    if ($null -eq $content) { return '' }
    if ($content -is [string]) { return [string]$content }
    try {
        return [string]($content.ReadAsStringAsync().GetAwaiter().GetResult())
    } catch { }
    return ''
}

function New-HttpParams([string]$Uri, [string]$Method, [string]$Key, [string]$Body, [int]$Timeout) {
    $params = @{
        Uri         = $Uri
        Method      = $Method
        Headers     = @{ Authorization = "Bearer $Key" }
        ContentType = 'application/json'
        TimeoutSec  = $Timeout
        ErrorAction = 'Stop'
    }
    if (-not [string]::IsNullOrEmpty($Body)) { $params['Body'] = $Body }
    if ($PSVersionTable.PSVersion.Major -lt 6) { $params['UseBasicParsing'] = $true }
    return $params
}

function Read-ErrorResponseBody($ErrorRecord) {
    $body = ''
    try { $body = [string]$ErrorRecord.ErrorDetails.Message } catch { }

    $response = $null
    if ($null -ne $ErrorRecord -and $null -ne $ErrorRecord.Exception) {
        try { $response = $ErrorRecord.Exception.Response } catch { }
    }
    if ($null -ne $response -and [string]::IsNullOrEmpty($body)) {
        # Windows PowerShell 5.1 - System.Net.WebResponse
        try {
            $stream = $response.GetResponseStream()
            if ($null -ne $stream) {
                $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8)
                $body = $reader.ReadToEnd()
                $reader.Dispose()
            }
        } catch { }
        # PowerShell 7+ - System.Net.Http.HttpResponseMessage
        if ([string]::IsNullOrEmpty($body)) {
            try {
                $content = $response.Content
                if ($null -ne $content) {
                    $body = $content.ReadAsStringAsync().GetAwaiter().GetResult()
                }
            } catch { }
        }
        if ([string]::IsNullOrEmpty($body)) {
            try {
                $streamAsync = $response.Content.ReadAsStreamAsync()
                if ($null -ne $streamAsync) {
                    $stream2 = $streamAsync.GetAwaiter().GetResult()
                    if ($null -ne $stream2) {
                        $reader2 = New-Object System.IO.StreamReader($stream2, [System.Text.Encoding]::UTF8)
                        $body = $reader2.ReadToEnd()
                        $reader2.Dispose()
                    }
                }
            } catch { }
        }
    }
    return $body
}

function Get-ResponseStatusCode($ErrorRecord) {
    $response = $null
    if ($null -ne $ErrorRecord -and $null -ne $ErrorRecord.Exception) {
        try { $response = $ErrorRecord.Exception.Response } catch { }
    }
    if ($null -ne $response) {
        try { return [int]$response.StatusCode } catch { }
        try { return [int]($response.StatusCode.value__) } catch { }
    }
    return 0
}

function Test-InvalidTokenText([string]$Body) {
    if ([string]::IsNullOrEmpty($Body)) { return $false }
    $patterns = @(
        'invalid token',
        'invalid_token',
        'invalid api key',
        'invalid_api_key',
        'invalid api-key',
        'invalid x-api-key',
        'invalid key',
        'invalid authorization',
        'authentication_error',
        'invalid authentication',
        'unauthorized',
        'api key not found',
        'token not found',
        'token is invalid',
        'key is invalid',
        'неверный токен',
        'неверный ключ',
        'недействительный токен'
    )
    $lower = $Body.ToLowerInvariant()
    foreach ($pattern in $patterns) {
        if ($lower.Contains($pattern)) { return $true }
    }
    return $false
}

function Test-ModelNotFoundText([string]$Body) {
    if ([string]::IsNullOrEmpty($Body)) { return $false }
    $lower = $Body.ToLowerInvariant()
    foreach ($pattern in @('model not found', 'unknown model', 'does not exist', 'invalid model', 'model_not_found', 'no such model', 'модель не найдена')) {
        if ($lower.Contains($pattern)) { return $true }
    }
    return $false
}

function Test-MirageKey([string]$Key, [string]$ModelId) {
    $bodyText = @{
        model      = $ModelId
        messages   = @(@{ role = 'user'; content = 'Reply with OK.' })
        max_tokens = 1
        stream     = $false
    } | ConvertTo-Json -Depth 8 -Compress

    $params   = New-HttpParams -Uri $ChatUrl -Method 'Post' -Key $Key -Body $bodyText -Timeout 120
    $status   = 0
    $errorMsg = $null
    $respBody = $null
    $attempts = 0

    for ($attempt = 1; $attempt -le 3; $attempt++) {
        $attempts = $attempt
        $status   = 0
        $errorMsg = $null
        $respBody = $null
        try {
            $response = Invoke-WebRequest @params
            $status   = [int]$response.StatusCode
            $respBody = Read-ResponseContent $response
            break
        } catch {
            $errorMsg = $_.Exception.Message
            $respBody = Read-ErrorResponseBody $_
            $status   = Get-ResponseStatusCode $_
            $retryable = ($status -eq 0 -or $status -eq 429 -or $status -eq 500 -or $status -eq 502 -or $status -eq 503 -or $status -eq 504)
            if ($retryable -and $attempt -lt 3) {
                $delay = if ($attempt -eq 1) { 3 } else { 7 }
                if ($status -gt 0) {
                    Write-Host "  Attempt $attempt/3 failed : HTTP $status - retrying in $delay sec..." -ForegroundColor DarkGray
                } else {
                    Write-Host "  Attempt $attempt/3 failed : network error - retrying in $delay sec..." -ForegroundColor DarkGray
                }
                Start-Sleep -Seconds $delay
                continue
            }
            break
        }
    }

    $kind = 'Unknown'
    if (Test-InvalidTokenText $respBody) {
        $kind = 'InvalidToken'
    } elseif ($status -eq 0) {
        $kind = 'Network'
    } elseif ($status -eq 401 -or $status -eq 403) {
        $kind = 'Auth'
    } elseif ($status -eq 429) {
        $kind = 'RateLimit'
    } elseif (Test-ModelNotFoundText $respBody) {
        $kind = 'BadModel'
    } elseif ($status -ge 500) {
        $kind = 'Server'
    } elseif ($status -ge 200 -and $status -lt 300) {
        $kind = 'Ok'
    }

    return [pscustomobject]@{
        Status   = $status
        Body     = $respBody
        Error    = $errorMsg
        Attempts = $attempts
        Kind     = $kind
    }
}

function Get-MirageModelIds([string]$Key) {
    $params = New-HttpParams -Uri $ModelsUrl -Method 'Get' -Key $Key -Body $null -Timeout 45
    $response = Invoke-WebRequest @params
    $parsed = ConvertFrom-Json -InputObject (Read-ResponseContent $response) -ErrorAction Stop
    $ids = New-Object 'System.Collections.Generic.List[string]'
    $data = Get-JsonValue $parsed 'data'
    if ($null -ne $data) {
        foreach ($item in @($data)) {
            $id = [string](Get-JsonValue $item 'id')
            if (-not [string]::IsNullOrEmpty($id)) { [void]$ids.Add($id) }
        }
    }
    return ,$ids.ToArray()
}

function Test-ResponsesEndpoint([string]$Key, [string]$ModelId) {
    $bodyText = @{
        model              = $ModelId
        input              = 'ping'
        max_output_tokens  = 1
        stream             = $false
    } | ConvertTo-Json -Depth 8 -Compress
    $params = New-HttpParams -Uri $ResponsesUrl -Method 'Post' -Key $Key -Body $bodyText -Timeout 60
    try {
        $response = Invoke-WebRequest @params
        return [pscustomobject]@{ Available = $true; Status = [int]$response.StatusCode; Detail = '' }
    } catch {
        $status = Get-ResponseStatusCode $_
        return [pscustomobject]@{ Available = $false; Status = $status; Detail = $_.Exception.Message }
    }
}

function Format-BodyPreview([string]$Body) {
    if ([string]::IsNullOrEmpty($Body)) { return '' }
    $text = $Body.Trim()
    if ($text.Length -gt 400) { $text = $text.Substring(0, 400) + '...' }
    return $text
}

# ---------------------------------------------------------------------------
# Environment cleanup (ANTHROPIC_API_KEY)
# ---------------------------------------------------------------------------
function Get-MaskedValue([string]$Value) {
    if ([string]::IsNullOrEmpty($Value)) { return '' }
    if ($Value.Length -le 8) { return ('*' * $Value.Length) }
    return ($Value.Substring(0, 7) + ('*' * ([Math]::Min($Value.Length - 7, 24))))
}

# ---------------------------------------------------------------------------
# Client writers
# ---------------------------------------------------------------------------
function Set-ClaudeCodeSettings([string]$Path, [string]$Key, [string]$ModelId) {
    Update-JsonSettings $Path {
        param($settings)
        $envSettings = Get-OrCreate-JsonObject $settings 'env'
        Set-JsonValue $envSettings 'CLAUDE_CODE_MAX_CONTEXT_TOKENS' '1000000'
        Set-JsonValue $envSettings 'ANTHROPIC_BASE_URL' $AnthropicBaseUrl
        Set-JsonValue $envSettings 'ANTHROPIC_AUTH_TOKEN' $Key
        Set-JsonValue $envSettings 'ANTHROPIC_MODEL' ($ModelId + '[1m]')
        Set-JsonValue $envSettings 'ANTHROPIC_SMALL_FAST_MODEL' $ModelId
        Set-JsonValue $envSettings 'ANTHROPIC_DEFAULT_OPUS_MODEL' 'claude-opus-5.5'
        Set-JsonValue $envSettings 'ANTHROPIC_DEFAULT_SONNET_MODEL' 'claude-sonnet-5.5'
        Set-JsonValue $envSettings 'ANTHROPIC_DEFAULT_HAIKU_MODEL' 'claude-haiku-5.5'
        Set-JsonValue $settings 'model' ($ModelId + '[1m]')
        $picker = Get-OrCreate-JsonObject $settings 'modelPicker'
        $existing = Get-JsonValue $picker 'options'
        if ($null -ne $existing -and $existing -isnot [array]) { throw 'Existing Claude modelPicker.options is not an array; not modified.' }
        $mine = @($ModelCatalog | ForEach-Object { $_.Id + '[1m]' })
        $other = @($existing | Where-Object { $null -ne $_ -and $mine -notcontains (Get-JsonValue $_ 'model') })
        $rows = @($ModelCatalog | ForEach-Object { [pscustomobject]@{ model = ($_.Id + '[1m]'); label = ('Mirage - ' + $_.Name) } })
        Set-JsonValue $picker 'options' @($other + $rows)
    }
}


function New-CodexModelCatalog([string]$Path, [array]$Catalog) {
    $command = Get-Command codex -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($null -eq $command) { throw 'Install Codex CLI first: a bundled model template is required.' }
    $tmpHome = Join-Path ([IO.Path]::GetTempPath()) ('mirage-codex-' + [Guid]::NewGuid().ToString('N'))
    New-Item -ItemType Directory -Path $tmpHome -ErrorAction Stop | Out-Null
    $previousHome = $env:CODEX_HOME; $oldPreference = $ErrorActionPreference
    try {
        $env:CODEX_HOME = $tmpHome
        $ErrorActionPreference = 'Continue'
        $lines = & $command.Source debug models --bundled 2>$null
        $code = $LASTEXITCODE
    } finally {
        $ErrorActionPreference = $oldPreference
        if ($null -eq $previousHome) { Remove-Item Env:CODEX_HOME -ErrorAction SilentlyContinue } else { $env:CODEX_HOME = $previousHome }
        Remove-Item -LiteralPath $tmpHome -Recurse -Force -ErrorAction SilentlyContinue
    }
    if ($code -ne 0) { throw 'Codex could not export its bundled catalog; update Codex and try again.' }
    try { $bundled = ConvertFrom-Json -InputObject ($lines -join "`n") -ErrorAction Stop } catch { throw 'Codex returned an unreadable bundled model catalog.' }
    $templates = Get-JsonValue $bundled 'models'
    if ($templates -isnot [array] -or $templates.Count -lt 1) { throw 'Codex bundled catalog is empty or unsupported.' }
    $models = @($Catalog | ForEach-Object {
        $m = $_
        $clone = ConvertFrom-Json -InputObject (ConvertTo-Json -InputObject $templates[0] -Depth 100 -Compress) -ErrorAction Stop
        Set-JsonValue $clone 'slug' $m.Id
        Set-JsonValue $clone 'display_name' $m.Name
        Set-JsonValue $clone 'description' ('Mirage API - ' + $m.Name)
        Set-JsonValue $clone 'context_window' ([int]1000000)
        Set-JsonValue $clone 'max_context_window' ([int]1000000)
        Set-JsonValue $clone 'visibility' 'list'
        Set-JsonValue $clone 'supported_in_api' $true
        Set-JsonValue $clone 'priority' ([int]$m.Index)
        Set-JsonValue $clone 'availability_nux' $null
        Set-JsonValue $clone 'upgrade' $null
        $clone
    })
    $result = [pscustomobject]@{ models = $models }
    if (Test-Path -LiteralPath $Path -PathType Leaf) { Backup-File $Path }
    Write-AtomicText $Path (ConvertTo-JsonText $result)
}

function Set-CodexUserKey([string]$Name, [string]$Key) {
    [Environment]::SetEnvironmentVariable($Name, $Key, 'User')
    if ([Environment]::GetEnvironmentVariable($Name, 'User') -ne $Key) {
        throw 'Could not verify the saved user environment variable.'
    }
}
function Set-CodexConfig([string]$Path, [string]$ModelId, [string]$Key) {
    Ensure-ParentDirectory $Path
    $tomlText = ''
    if (Test-Path -LiteralPath $Path -PathType Leaf) {
        $tomlText = [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
        Backup-File $Path
    }

    $catalogPath = Join-Path (Split-Path -Parent $Path) 'mirage-model-catalog.json'
    $top = ([regex]::Split($tomlText, '(?m)^\s*\[', 2))[0]
    $existingCatalog = [regex]::Match($top, '(?m)^\s*model_catalog_json\s*=\s*(.+?)\s*(?:#.*)?$')
    if ($existingCatalog.Success -and $existingCatalog.Groups[1].Value.Trim() -ne (Quote-TomlString $catalogPath)) {
        throw 'An existing Codex custom model catalog is configured. It was not replaced; merge Mirage models manually.'
    }
    New-CodexModelCatalog $catalogPath $ModelCatalog
    # Codex reads these root keys; keep them in the top-level scope.
    $rootFields = [ordered]@{
        model          = (Quote-TomlString $ModelId)
        model_provider = (Quote-TomlString $ProviderId)
        model_context_window = '1000000'
        model_catalog_json = (Quote-TomlString $catalogPath)
    }
    $providerFields = [ordered]@{
        name                 = (Quote-TomlString $ProviderLabel)
        base_url             = (Quote-TomlString $BaseUrl)
        env_key              = (Quote-TomlString $EnvKeyName)
        wire_api             = (Quote-TomlString 'responses')
        requires_openai_auth = 'false'
    }

    $tomlText = Set-TomlFields $tomlText '' $rootFields
    $tomlText = Set-TomlFields $tomlText ('model_providers.' + $ProviderId) $providerFields
    $oldUserKey = [Environment]::GetEnvironmentVariable($EnvKeyName, 'User')
    $oldProcessKey = [Environment]::GetEnvironmentVariable($EnvKeyName, 'Process')
    try {
        Set-CodexUserKey $EnvKeyName $Key
        Set-Item -LiteralPath ('Env:' + $EnvKeyName) -Value $Key -ErrorAction Stop
        Write-AtomicText $Path $tomlText
    } catch {
        try { [Environment]::SetEnvironmentVariable($EnvKeyName, $oldUserKey, 'User') } catch { }
        if ($null -eq $oldProcessKey) { Remove-Item -LiteralPath ('Env:' + $EnvKeyName) -ErrorAction SilentlyContinue }
        else { Set-Item -LiteralPath ('Env:' + $EnvKeyName) -Value $oldProcessKey -ErrorAction SilentlyContinue }
        throw 'Could not persist the Codex key or config. See backups; configuration may be incomplete.'
    }
    Publish-EnvironmentChange

    $authPath = Join-Path (Split-Path -Parent $Path) 'auth.json'
    if (Test-Path -LiteralPath $authPath -PathType Leaf) {
        Write-Host "    Note: $authPath exists. Codex will use $EnvKeyName with this config. Existing auth.json was not changed." -ForegroundColor DarkGray
    }
}

function Set-AiSdkProviderConfig([string]$Path, [string]$SchemaUrl, [string]$Key, $SelectedEntry, [array]$Catalog) {
    $allModels = Get-EffectiveCatalog $Catalog $SelectedEntry
    Update-JsonSettings $Path {
        param($settings)
        if (-not [string]::IsNullOrEmpty($SchemaUrl) -and $null -eq (Get-JsonValue $settings '$schema')) {
            Set-JsonValue $settings '$schema' $SchemaUrl
        }
        $providers = Get-OrCreate-JsonObject $settings 'provider'
        $mirage = Get-OrCreate-JsonObject $providers $ProviderId
        Set-JsonValue $mirage 'npm' '@ai-sdk/openai-compatible'
        Set-JsonValue $mirage 'name' $ProviderLabel
        $allowed = @($Catalog | ForEach-Object { $_.Id })
        $blacklist = Get-JsonValue $mirage 'blacklist'
        foreach ($id in @($blacklist)) { if ($null -ne $id -and $allowed -contains $id) { throw 'Existing Mirage blacklist excludes a required model; not modified.' } }
        Set-JsonValue $mirage 'whitelist' $allowed
        $options = Get-OrCreate-JsonObject $mirage 'options'
        Set-JsonValue $options 'baseURL' $BaseUrl
        Set-JsonValue $options 'apiKey' $Key
        $modelsObj = Get-OrCreate-JsonObject $mirage 'models'
        foreach ($m in $allModels) {
            $entry = Get-OrCreate-JsonObject $modelsObj $m.Id
            Set-JsonValue $entry 'name' $m.Name
            Set-JsonValue $entry 'tool_call' $true
            $limitObj = Get-OrCreate-JsonObject $entry 'limit'
            Set-JsonValue $limitObj 'context' ([int]$m.Context)
            Set-JsonValue $limitObj 'output' ([int]$m.MaxOut)
            if ($m.Vision) {
                Set-JsonValue $entry 'attachment' $true
                Set-JsonValue $entry 'modalities' ([pscustomobject]@{
                    input  = @('text', 'image')
                    output = @('text')
                })
            }
        }
        Set-JsonValue $settings 'model' ($ProviderId + '/' + $SelectedEntry.Id)
    }
}

function Set-ClineProvidersConfig([string]$Path, [string]$Key, [string]$ModelId) {
    Update-JsonSettings $Path {
        param($settings)
        if ($null -eq (Get-JsonValue $settings 'version')) {
            Set-JsonValue $settings 'version' ([int]1)
        }

        Set-JsonValue $settings 'lastUsedProvider' 'openai-compatible'
        if ($null -eq (Get-JsonValue $settings 'modes')) {
            Set-JsonValue $settings 'modes' ([pscustomobject]@{})
        }
        $providers = Get-OrCreate-JsonObject $settings 'providers'
        $oai = Get-OrCreate-JsonObject $providers 'openai-compatible'
        $inner = Get-OrCreate-JsonObject $oai 'settings'
        Set-JsonValue $inner 'provider' 'openai-compatible'
        Set-JsonValue $inner 'apiKey' $Key
        Set-JsonValue $inner 'model' $ModelId
        Set-JsonValue $inner 'baseUrl' $BaseUrl
        Set-JsonValue $inner 'contextWindow' ([int]1000000)
        $utcIso = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ', [System.Globalization.CultureInfo]::InvariantCulture)
        Set-JsonValue $oai 'updatedAt' $utcIso
        Set-JsonValue $oai 'tokenSource' 'manual'
    }
}

function Set-ClineStateAndSecrets([string]$GlobalStatePath, [string]$SecretsPath, [string]$Key, [string]$ModelId) {
    Update-JsonSettings $GlobalStatePath {
        param($gs)
        Set-JsonValue $gs 'welcomeViewCompleted' $true
        Set-JsonValue $gs 'apiProvider' 'openai'
        Set-JsonValue $gs 'planModeApiProvider' 'openai'
        Set-JsonValue $gs 'actModeApiProvider' 'openai'
        Set-JsonValue $gs 'openAiBaseUrl' $BaseUrl
        Set-JsonValue $gs 'openAiModelId' $ModelId
        Set-JsonValue $gs 'planModeOpenAiModelId' $ModelId
        Set-JsonValue $gs 'actModeOpenAiModelId' $ModelId
        foreach ($infoKey in @('openAiModelInfo', 'planModeOpenAiModelInfo', 'actModeOpenAiModelInfo')) {
            $info = Get-OrCreate-JsonObject $gs $infoKey
            Set-JsonValue $info 'contextWindow' ([int]1000000)
        }
        Set-JsonValue $gs 'lastUsedProvider' 'openai-compatible'
    }
    if (-not [string]::IsNullOrEmpty($SecretsPath)) {
        Update-JsonSettings $SecretsPath {
            param($sec)
            Set-JsonValue $sec 'openAiApiKey' $Key
        }
    }
}

function Set-ClineFullConfig([string]$UserHome, [string]$AppDataRoot, [string]$Key, [string]$ModelId) {
    $clineDataDir = $env:CLINE_DATA_DIR
    if ([string]::IsNullOrWhiteSpace($clineDataDir)) {
        $clineRoot = $env:CLINE_DIR
        if ([string]::IsNullOrWhiteSpace($clineRoot)) {
            $clineRoot = Join-Path $UserHome '.cline'
        }
        $clineDataDir = Join-Path $clineRoot 'data'
    }

    $clineProvidersPath = Join-Path $clineDataDir 'settings\providers.json'
    Set-ClineProvidersConfig $clineProvidersPath $Key $ModelId

    $sharedGlobalState = Join-Path $clineDataDir 'globalState.json'
    $sharedSecrets     = Join-Path $clineDataDir 'secrets.json'
    Set-ClineStateAndSecrets $sharedGlobalState $sharedSecrets $Key $ModelId


}

function New-RooProfile([string]$Id, [string]$Key, $Model) {
    return [pscustomobject]@{
        id                    = $Id
        apiProvider           = 'openai'
        openAiBaseUrl         = $BaseUrl
        openAiApiKey          = $Key
        openAiModelId         = $Model.Id
        apiModelId            = $Model.Id
        openAiStreamingEnabled = $true
        openAiCustomModelInfo = [pscustomobject]@{
            maxTokens           = [int]$Model.MaxOut
            contextWindow       = [int]$Model.Context
            supportsImages      = [bool]$Model.Vision
            supportsPromptCache = $false
        }
    }
}

function Set-RooAutoImportFile([string]$Path, [string]$Key, $SelectedEntry, [array]$Catalog) {
    $allModels = Get-EffectiveCatalog $Catalog $SelectedEntry
    Update-JsonSettings $Path {
        param($settings)
        $profiles = Get-OrCreate-JsonObject $settings 'providerProfiles'
        Set-JsonValue $profiles 'currentApiConfigName' 'Mirage API'
        $configs = Get-OrCreate-JsonObject $profiles 'apiConfigs'

        Set-JsonValue $configs 'Mirage API' (New-RooProfile 'mirage-default' $Key $SelectedEntry)

        foreach ($m in $allModels) {
            $profileName = 'Mirage - ' + $m.Name
            $safeId = 'mirage-' + ($m.Id -replace '[^a-zA-Z0-9_-]', '-')
            Set-JsonValue $configs $profileName (New-RooProfile $safeId $Key $m)
        }
    }
}

function Test-AnthropicEndpoint([string]$Key, [string]$ModelId) {
    $body = @{ model=$ModelId; messages=@(@{role='user';content='Reply OK.'}); max_tokens=1 } | ConvertTo-Json -Depth 8 -Compress
    try {
        $params = @{ Uri=($AnthropicBaseUrl + '/v1/messages'); Method='Post'; Headers=@{ 'x-api-key'=$Key; 'anthropic-version'='2023-06-01' }; ContentType='application/json'; Body=$body; TimeoutSec=120; ErrorAction='Stop' }
        if ($PSVersionTable.PSVersion.Major -lt 6) { $params['UseBasicParsing'] = $true }
        $response = Invoke-WebRequest @params
        return [bool]([int]$response.StatusCode -ge 200 -and [int]$response.StatusCode -lt 300)
    } catch { return $false }
}
function Set-ZCodePersonalRegistry([string]$Path, [string]$Key, $SelectedEntry, [array]$Catalog) {
    Update-JsonSettings $Path {
        param($root)
        $version = Get-JsonValue $root 'schemaVersion'
        if ($null -eq $version) {
            if (@($root.PSObject.Properties).Count -gt 0) { throw 'Unknown ZCode registry format; not modified.' }
            Set-JsonValue $root 'schemaVersion' 1
        } elseif ($version -ne 1) { throw 'Unsupported ZCode registry version; not modified.' }
        $config = Get-OrCreate-JsonObject $root 'config'
        $providers = Get-OrCreate-JsonObject $config 'providerConfigRules'
        $existingProviders = Get-JsonValue $providers 'providerRules'
        if ($null -ne $existingProviders -and $existingProviders -isnot [array]) { throw 'Invalid ZCode providerRules; not modified.' }
        $models = Get-OrCreate-JsonObject $config 'modelConfigRules'
        $existingModels = Get-JsonValue $models 'providerModelRules'
        $manual = Get-JsonValue $models 'manualProviderModelRules'
        if ($null -ne $existingModels -and $existingModels -isnot [array]) { throw 'Invalid ZCode providerModelRules; not modified.' }
        if ($null -ne $manual -and $manual -isnot [array]) { throw 'Invalid ZCode manualProviderModelRules; not modified.' }
        $id = 'mirage-setup'
        foreach ($rule in @($manual)) {
            if ($null -ne $rule -and (Get-JsonValue $rule 'providerId') -eq $id) { throw 'Manual Mirage model overrides already exist; left unchanged.' }
        }
        $modelIds = @($Catalog | ForEach-Object { $_.Id })
        $providerRule = [pscustomobject]@{
            providerId = $id; providerName = 'Mirage API'; enabled = $true
            config = [pscustomobject]@{
                group = 'standard-personal'
                access = [pscustomobject]@{ type = 'api-key'; apiKey = $Key }
                api = [pscustomobject]@{ type = 'openai-chat-completions'; baseUrl = $BaseUrl }
                personalModelIds = $modelIds; modelOrder = $modelIds
            }
        }
        $otherProviders = @($existingProviders | Where-Object { $null -ne $_ -and (Get-JsonValue $_ 'providerId') -ne $id })
        Set-JsonValue $providers 'providerRules' @($otherProviders + @($providerRule))
        $otherModels = @($existingModels | Where-Object { $null -ne $_ -and (Get-JsonValue $_ 'providerId') -ne $id })
        $ourModels = @($Catalog | ForEach-Object {
            [pscustomobject]@{ providerId = $id; modelId = $_.Id; config = [pscustomobject]@{
                properties = [pscustomobject]@{ contextWindow = 1000000 }
                optionSpecs = [pscustomobject]@{ maxOutputTokens = [pscustomobject]@{ max = [int]$_.MaxOut } }
            } }
        })
        Set-JsonValue $models 'providerModelRules' @($otherModels + $ourModels)
        if ($null -eq $manual) { Set-JsonValue $models 'manualProviderModelRules' @() }
        $order = Get-JsonValue $config 'providerOrder'
        if ($null -ne $order -and $order -isnot [array]) { throw 'Invalid ZCode providerOrder; not modified.' }
        $orderList = @($order | Where-Object { $null -ne $_ -and $_ -ne $id })
        Set-JsonValue $config 'providerOrder' @($orderList + @($id))
        Set-JsonValue $config 'defaultModelSelection' ([pscustomobject]@{providerId=$id;modelId=$SelectedEntry.Id})
    }
}
function Set-RooImportInEditor([string]$Path, [string]$ImportPath) {
    Update-JsonSettings $Path {
        param($settings)
        $old = [string](Get-JsonValue $settings 'roo-cline.autoImportSettingsPath')
        if (-not [string]::IsNullOrWhiteSpace($old) -and $old -ne $ImportPath) {
            throw 'Roo Code already has an auto-import path. It was left untouched; import the generated file manually.'
        }
        Set-JsonValue $settings 'roo-cline.autoImportSettingsPath' $ImportPath
    }
}
function Write-MirageCatalog([string]$Path, [array]$Catalog) {
    $lines = @('Mirage API: model IDs', 'Context setting: 1000000 tokens (actual upstream limit may be lower)', '')
    foreach ($m in $Catalog) { $lines += $m.Id }
    if (Test-Path -LiteralPath $Path -PathType Leaf) { Backup-File $Path }
    Write-AtomicText $Path (($lines -join "`r`n") + "`r`n")
}

# ---------------------------------------------------------------------------
# MAIN: configure only the client selected by the user.
# ---------------------------------------------------------------------------
try {
    $userHome = Get-UserHomeDirectory
    $appDataRoot = Get-AppDataDirectory
    $configRoot = Get-ConfigRootDirectory
    $mirageDir = Join-Path $userHome '.mirage'
    Write-Section 'Mirage API setup v2.1 BETA — предварительная версия'
    Write-Host 'Выберите один клиент. Остальные клиенты не изменяются.' -ForegroundColor Cyan
    Write-Host '  1 Claude Code     2 Codex          3 Cline'
    Write-Host '  4 Roo Code        5 Kilo CLI      6 OpenCode'
    Write-Host '  7 ZCode           8 Cursor (ручная настройка)'
    $client = Read-Host 'Номер клиента (1–8; Enter — отмена)'
    if ($client -notmatch '^[1-8]$') { Write-Host 'Отменено. Ничего не изменено.'; exit $EXIT_CANCELLED }
    if ($client -eq '2') {
        Write-Host 'Codex 0.165 может получать ошибки при вызове инструментов. Настройка файла сама по себе не исправляет совместимость.' -ForegroundColor Yellow
    }
    $runningEditors = Get-RunningEditorProcesses
    if ($runningEditors.Count -gt 0 -and $client -in @('3','4','5','7','8')) {
        Show-RunningEditorsWarning $runningEditors
        $answer = Read-Host 'Закройте редакторы и введите r для повторной проверки; любой другой ответ отменяет установку'
        if ($answer -ne 'r' -or (Get-RunningEditorProcesses).Count -gt 0) { exit $EXIT_CANCELLED }
    }
    Write-Section 'Выберите модель по умолчанию (в каталоге только эти 16 ID):'
    foreach ($m in $ModelCatalog) { Write-Host ('  {0,2}. {1} [1M client setting]' -f $m.Index, $m.Id) }
    $choice = Read-Host 'Номер модели 1–16 (Enter = 1)'
    if ([string]::IsNullOrWhiteSpace($choice)) { $choice = '1' }
    $num = 0
    if (-not [int]::TryParse($choice, [ref]$num) -or $num -lt 1 -or $num -gt 16) { Write-Host 'Неверный выбор. Ничего не изменено.'; exit $EXIT_CANCELLED }
    $selectedEntry = $ModelCatalog[$num - 1]; $selectedModel = $selectedEntry.Id
    Write-Host 'Внимание: 1 000 000 — настройка клиента, а не гарантия реального лимита модели на сервере.' -ForegroundColor Yellow
    $secureKey = Read-Host -Prompt 'Введите ключ Mirage API (ввод скрыт)' -AsSecureString
    if ($null -eq $secureKey -or $secureKey.Length -eq 0) { exit $EXIT_NO_KEY }
    $ptr = [IntPtr]::Zero
    try { $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey); $apiKey = ([Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)).Trim() }
    finally { if ($ptr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) } }
    if ($apiKey -notmatch '^sk-\S+$') { Write-Host 'Неверный формат ключа; ничего не изменено.'; exit $EXIT_NO_KEY }
    Write-Section 'Проверяем ключ и выбранную модель (ответ может занять до 120 секунд)...'
    $keyTest = Test-MirageKey -Key $apiKey -ModelId $selectedModel
    if ($keyTest.Kind -in @('InvalidToken','Auth')) { Write-Host 'Шлюз отклонил ключ. Ничего не изменено.' -ForegroundColor Red; exit $EXIT_AUTH }
    if ($keyTest.Kind -eq 'BadModel') { Write-Host 'Выбранная модель недоступна. Ничего не изменено.' -ForegroundColor Red; exit $EXIT_BAD_MODEL }
    if ($keyTest.Kind -ne 'Ok') { Write-Host ('Gateway test failed (HTTP {0}). Nothing changed.' -f $keyTest.Status) -ForegroundColor Red; exit $EXIT_NETWORK }
    if ($client -eq '1' -and -not (Test-AnthropicEndpoint -Key $apiKey -ModelId $selectedModel)) {
        Write-Host 'Anthropic Messages API не прошёл проверку. Claude Code не изменён.' -ForegroundColor Yellow; exit $EXIT_NETWORK
    }
    if ($client -eq '2') {
        $probe = Test-ResponsesEndpoint -Key $apiKey -ModelId $selectedModel
        if (-not $probe.Available) { Write-Host 'Внимание: базовая проверка Responses API не прошла. По вашему выбору настройки будут записаны, но могут не работать.' -ForegroundColor Yellow }
        $currentKey = [Environment]::GetEnvironmentVariable($EnvKeyName, 'User')
        $processKey = [Environment]::GetEnvironmentVariable($EnvKeyName, 'Process')
        $machineKey = [Environment]::GetEnvironmentVariable($EnvKeyName, 'Machine')
        if ((-not [string]::IsNullOrEmpty($currentKey) -and $currentKey -ne $apiKey) -or
            (-not [string]::IsNullOrEmpty($processKey) -and $processKey -ne $apiKey) -or
            (-not [string]::IsNullOrEmpty($machineKey) -and $machineKey -ne $apiKey)) {
            $EnvKeyName = 'MIRAGE_CODEX_API_KEY_' + [Guid]::NewGuid().ToString('N').Substring(0,8).ToUpperInvariant()
        }
    }
    Write-Host 'Файлы настроек и их резервные копии могут содержать ключ открытым текстом. Не передавайте их другим.' -ForegroundColor Yellow
    Write-Section 'Настройка выбранного клиента'
    $manual = $false
    switch ($client) {
        '1' {
            Invoke-ConfigStep 'Claude Code' { Set-ClaudeCodeSettings (Join-Path $userHome '.claude\settings.json') $apiKey $selectedModel }
            Write-Host 'Список из 16 моделей требует Claude Code v2.1.242+. Для контекста 1M используется [1m].' -ForegroundColor Yellow
            Write-Host 'Настройка 1M не увеличивает реальный лимит модели на сервере.' -ForegroundColor Yellow
            Write-Host 'Существующие переменные ANTHROPIC_API_KEY сохранены и могут мешать работе со шлюзом.' -ForegroundColor Yellow
        }
        '2' {
            $codexHome = $env:CODEX_HOME
            if ([string]::IsNullOrWhiteSpace($codexHome)) { $codexHome = Join-Path $userHome '.codex' }
            Invoke-ConfigStep 'Codex CLI' { Set-CodexConfig (Join-Path $codexHome 'config.toml') $selectedModel $apiKey }
            Write-Host 'Codex: другую модель можно выбрать через --model MODEL_ID. Настройка контекста общая.' -ForegroundColor DarkGray
            Write-Host 'Каталог Mirage заменяет встроенный список моделей Codex; настройки других провайдеров сохраняются.' -ForegroundColor Yellow
            Write-Host 'Откройте новый терминал, чтобы загрузить переменную ключа.' -ForegroundColor Yellow
            Write-Host 'Внимание: запись настроек не гарантирует работу инструментов Codex 0.165 с текущим шлюзом.' -ForegroundColor Yellow
        }
        '3' {
            Invoke-ConfigStep 'Cline shared CLI settings' { Set-ClineFullConfig $userHome $appDataRoot $apiKey $selectedModel }
            Write-Host 'Расширение Cline может использовать защищённое хранилище редактора. Проверьте провайдера и контекст в интерфейсе Cline.' -ForegroundColor Yellow
            Write-Host 'Cline может показывать дополнительные модели, полученные непосредственно от шлюза; установщик их не добавляет.' -ForegroundColor Yellow
            Write-Host 'Если параметры не применились, выберите OpenAI Compatible и задайте адрес, ID модели и контекст 1000000 в интерфейсе.' -ForegroundColor Yellow
        }
        '4' {
            $rooImportPath = Join-Path $mirageDir 'roo-code-settings.json'
            Invoke-ConfigStep 'Roo Code profiles' { Set-RooAutoImportFile $rooImportPath $apiKey $selectedEntry $ModelCatalog }
            $editorList = @(@{Folder='Code';Name='VS Code'},@{Folder='Cursor';Name='Cursor'},@{Folder='Windsurf';Name='Windsurf'},@{Folder='Code - Insiders';Name='VS Code Insiders'},@{Folder='VSCodium';Name='VSCodium'},@{Folder='Trae';Name='Trae'},@{Folder='Kiro';Name='Kiro'})
            $detected = @($editorList | Where-Object { Test-Path -LiteralPath (Join-Path $appDataRoot $_.Folder) -PathType Container })
            if ($detected.Count -eq 0) { Write-Host "No editor detected. Import manually: $rooImportPath" -ForegroundColor Yellow; $manual = $true }
            else {
                for ($i=0; $i -lt $detected.Count; $i++) { Write-Host ('  {0}. {1}' -f ($i+1),$detected[$i].Name) }
                $editorChoice = Read-Host 'Номер редактора (Enter — импорт вручную)'; $editorNum = 0
                if ([int]::TryParse($editorChoice,[ref]$editorNum) -and $editorNum -ge 1 -and $editorNum -le $detected.Count) {
                    $editorPath = Join-Path $appDataRoot ($detected[$editorNum-1].Folder + '\User\settings.json')
                    Invoke-ConfigStep 'Roo auto-import in selected editor' { Set-RooImportInEditor $editorPath $rooImportPath }
                } else { Write-Host "Import this file in Roo Code: $rooImportPath" -ForegroundColor Yellow; $manual = $true }
            }
        }
        '5' {
            $kiloDir = $env:KILO_CONFIG_DIR; if ([string]::IsNullOrWhiteSpace($kiloDir)) { $kiloDir = Join-Path $configRoot 'kilo' }
            Invoke-ConfigStep 'Kilo CLI provider' { Set-AiSdkProviderConfig (Join-Path $kiloDir 'kilo.jsonc') 'https://app.kilo.ai/config.json' $apiKey $selectedEntry $ModelCatalog }
            Write-Host 'Проверьте провайдера в Kilo: настройки проекта могут переопределять глобальные.' -ForegroundColor Yellow
        }
        '6' {
            $dir = Join-Path $configRoot 'opencode'
            $jsonc = Join-Path $dir 'opencode.jsonc'; $json = Join-Path $dir 'opencode.json'
            $path = if (Test-Path -LiteralPath $jsonc -PathType Leaf) { $jsonc } else { $json }
            Invoke-ConfigStep 'OpenCode provider and 16 models' { Set-AiSdkProviderConfig $path 'https://opencode.ai/config.json' $apiKey $selectedEntry $ModelCatalog }
            Write-Host 'OpenCode: откройте /models и выберите Mirage API.' -ForegroundColor DarkGray
        }
        '7' {
            $path = $env:ZCODE_PERSONAL_PROVIDER_CONFIG_FILE
            if ([string]::IsNullOrWhiteSpace($path)) {
                $base = $env:ZCODE_DATA_BASE_DIR
                if ([string]::IsNullOrWhiteSpace($base)) { $base = $userHome }
                $path = Join-Path $base '.zcode\v2\provider_config.json'
            }
            $legacyPath = Join-Path (Split-Path -Parent $path) 'config.json'
            if (-not (Test-Path -LiteralPath $path -PathType Leaf) -and (Test-Path -LiteralPath $legacyPath -PathType Leaf)) {
                throw 'Existing legacy ZCode config detected but registry is absent. Open ZCode 3.14+ once to migrate it, close ZCode, then retry. No ZCode files were changed.'
            }
            Invoke-ConfigStep 'ZCode personal registry and 16 models' { Set-ZCodePersonalRegistry $path $apiKey $selectedEntry $ModelCatalog }
            Write-Host 'ZCode 3.14+: откройте приложение и выберите Mirage API в списке моделей.' -ForegroundColor DarkGray
            Write-Host 'ZCode CLI 3.15.1 может показывать 200000, хотя рабочий лимит задан как 1000000.' -ForegroundColor Yellow
        }
        '8' {
            $manual = $true
            Write-Host 'Настройка самого Cursor требует его интерфейса:' -ForegroundColor Yellow
            Write-Host 'Settings > Models > API Keys: enable OpenAI API key and Base URL override.'
            Write-Host "Base URL: $BaseUrl. Enter your key there; add model IDs from the catalog file."
            Write-Host 'Cursor может не поддерживать автоматический импорт всех моделей и настройку окна 1M.' -ForegroundColor Yellow
        }
    }
    Invoke-ConfigStep 'Model ID list' { Write-MirageCatalog (Join-Path $mirageDir 'models.txt') $ModelCatalog }
    if ($failedSteps.Count -gt 0) { Write-Host ('Failed: ' + ($failedSteps -join ', ')) -ForegroundColor Red; exit $EXIT_PARTIAL }
    if ($manual) { Write-Host 'Остались ручные действия; см. инструкции выше.' -ForegroundColor Yellow; exit $EXIT_PARTIAL }
    Write-Host 'Настройки записаны. Перезапустите клиент и проверьте модель и контекст.' -ForegroundColor Green
    Write-Host 'Claude Code, OpenCode, Kilo, Roo и ZCode получают список моделей; в Cline выбор может потребовать ручных действий.' -ForegroundColor Yellow
    exit $EXIT_OK
} catch {
    Write-Host ('Setup failed: ' + $_.Exception.Message) -ForegroundColor Red
    exit $EXIT_FATAL
}
