param(
    [string]$CodexHome = (Join-Path $env:USERPROFILE ".codex"),
    [string]$QoderDataDir = (Join-Path $env:APPDATA "Qoder"),
    [string]$Model = "",
    [string]$BaseUrl = "",
    [string]$Alias = "",
    [string]$Provider = "openai",
    [string]$ByokTypeKey = "openai",
    [string]$ModelId = "",
    [switch]$AllowRunning
)

$ErrorActionPreference = "Stop"

if (-not ("System.Security.Cryptography.AesGcm" -as [type])) {
    $pwsh = Get-Command pwsh -ErrorAction SilentlyContinue
    if (-not $pwsh -and (Test-Path -LiteralPath "C:\Program Files\PowerShell\7\pwsh.exe")) {
        $pwsh = [pscustomobject]@{ Source = "C:\Program Files\PowerShell\7\pwsh.exe" }
    }
    if ($pwsh -and $PSCommandPath) {
        $pwshArgs = [System.Collections.Generic.List[string]]::new()
        @(
            "-NoProfile",
            "-ExecutionPolicy", "Bypass",
            "-File", $PSCommandPath,
            "-CodexHome", $CodexHome,
            "-QoderDataDir", $QoderDataDir
        ) | ForEach-Object { $pwshArgs.Add($_) }
        if (-not [string]::IsNullOrWhiteSpace($Model)) { $pwshArgs.Add("-Model"); $pwshArgs.Add($Model) }
        if (-not [string]::IsNullOrWhiteSpace($BaseUrl)) { $pwshArgs.Add("-BaseUrl"); $pwshArgs.Add($BaseUrl) }
        if (-not [string]::IsNullOrWhiteSpace($Alias)) { $pwshArgs.Add("-Alias"); $pwshArgs.Add($Alias) }
        if (-not [string]::IsNullOrWhiteSpace($Provider)) { $pwshArgs.Add("-Provider"); $pwshArgs.Add($Provider) }
        if (-not [string]::IsNullOrWhiteSpace($ByokTypeKey)) { $pwshArgs.Add("-ByokTypeKey"); $pwshArgs.Add($ByokTypeKey) }
        if (-not [string]::IsNullOrWhiteSpace($ModelId)) { $pwshArgs.Add("-ModelId"); $pwshArgs.Add($ModelId) }
        if ($AllowRunning) { $pwshArgs.Add("-AllowRunning") }
        & $pwsh.Source @pwshArgs
        exit $LASTEXITCODE
    }
    throw "System.Security.Cryptography.AesGcm is unavailable. Install PowerShell 7 or run this script with pwsh."
}

function Resolve-Tool {
    param(
        [string]$Name,
        [string[]]$Fallbacks
    )
    $cmd = Get-Command $Name -ErrorAction SilentlyContinue
    if ($cmd) { return $cmd.Source }
    foreach ($path in $Fallbacks) {
        if (Test-Path -LiteralPath $path) { return $path }
    }
    throw "Tool not found: $Name"
}

function SqlQuote {
    param([string]$Value)
    return "'" + $Value.Replace("'", "''") + "'"
}

function Get-TomlString {
    param(
        [string]$Text,
        [string]$Key
    )
    $match = [regex]::Match($Text, "(?m)^\s*$([regex]::Escape($Key))\s*=\s*""([^""]*)""")
    if ($match.Success) { return $match.Groups[1].Value }
    return ""
}

function Get-ProviderBaseUrl {
    param(
        [string]$ConfigText,
        [string]$ProviderName
    )
    if ([string]::IsNullOrWhiteSpace($ProviderName)) { return "" }
    $escaped = [regex]::Escape($ProviderName)
    $pattern = "(?ms)^\[model_providers\.$escaped\]\s*(.*?)(?=^\[|\z)"
    $section = [regex]::Match($ConfigText, $pattern)
    if (-not $section.Success) { return "" }
    return Get-TomlString -Text $section.Groups[1].Value -Key "base_url"
}

function ConvertTo-QoderEncryptedSecretJson {
    param(
        [string]$PlainText,
        [string]$LocalStatePath
    )

    $localState = Get-Content -LiteralPath $LocalStatePath -Raw | ConvertFrom-Json
    $encryptedKey = [Convert]::FromBase64String($localState.os_crypt.encrypted_key)
    if ($encryptedKey.Length -le 5) { throw "Invalid Qoder os_crypt encrypted_key." }

    $dpapiBlob = [byte[]]$encryptedKey[5..($encryptedKey.Length - 1)]
    Add-Type -AssemblyName System.Security
    $key = [System.Security.Cryptography.ProtectedData]::Unprotect(
        $dpapiBlob,
        $null,
        [System.Security.Cryptography.DataProtectionScope]::CurrentUser
    )

    $plain = [Text.Encoding]::UTF8.GetBytes($PlainText)
    $nonce = New-Object byte[] 12
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    try {
        $rng.GetBytes($nonce)
    }
    finally {
        $rng.Dispose()
    }
    $cipher = New-Object byte[] $plain.Length
    $tag = New-Object byte[] 16

    $aes = [System.Security.Cryptography.AesGcm]::new($key)
    try {
        $aes.Encrypt($nonce, $plain, $cipher, $tag, $null)
    }
    finally {
        $aes.Dispose()
    }

    $encrypted = New-Object byte[] (3 + $nonce.Length + $cipher.Length + $tag.Length)
    $prefix = [byte[]][char[]]"v10"
    [Array]::Copy($prefix, 0, $encrypted, 0, 3)
    [Array]::Copy($nonce, 0, $encrypted, 3, $nonce.Length)
    [Array]::Copy($cipher, 0, $encrypted, 15, $cipher.Length)
    [Array]::Copy($tag, 0, $encrypted, 15 + $cipher.Length, $tag.Length)

    return @{
        Json = (@{ type = "Buffer"; data = @($encrypted) } | ConvertTo-Json -Compress)
        Key = $key
    }
}

function Test-QoderSecret {
    param(
        [string]$SecretJson,
        [byte[]]$Key,
        [string]$ExpectedPlainText
    )
    $obj = $SecretJson | ConvertFrom-Json
    $bytes = [byte[]]$obj.data
    if ($bytes.Length -lt 32) { return $false }
    $nonce = [byte[]]$bytes[3..14]
    $ctag = [byte[]]$bytes[15..($bytes.Length - 1)]
    $cipher = [byte[]]$ctag[0..($ctag.Length - 17)]
    $tag = [byte[]]$ctag[($ctag.Length - 16)..($ctag.Length - 1)]
    $plain = New-Object byte[] $cipher.Length
    $aes = [System.Security.Cryptography.AesGcm]::new($Key)
    try {
        $aes.Decrypt($nonce, $cipher, $tag, $plain, $null)
    }
    finally {
        $aes.Dispose()
    }
    return ([Text.Encoding]::UTF8.GetString($plain)) -eq $ExpectedPlainText
}

$qoderProcess = Get-Process | Where-Object { $_.ProcessName -match "Qoder|qoder" }
if ($qoderProcess -and -not $AllowRunning) {
    throw "Qoder is running. Close Qoder first, or rerun with -AllowRunning if you accept overwrite risk."
}

$sqlite = Resolve-Tool -Name "sqlite3" -Fallbacks @("D:\platform-tools\sqlite3.exe")
$configPath = Join-Path $CodexHome "config.toml"
$authPath = Join-Path $CodexHome "auth.json"
$dbPath = Join-Path $QoderDataDir "User\globalStorage\state.vscdb"
$localStatePath = Join-Path $QoderDataDir "Local State"

foreach ($path in @($configPath, $authPath, $dbPath, $localStatePath)) {
    if (-not (Test-Path -LiteralPath $path)) { throw "Required file not found: $path" }
}

$configText = Get-Content -LiteralPath $configPath -Raw
if ([string]::IsNullOrWhiteSpace($Model)) {
    $Model = Get-TomlString -Text $configText -Key "model"
}
if ([string]::IsNullOrWhiteSpace($BaseUrl)) {
    $modelProvider = Get-TomlString -Text $configText -Key "model_provider"
    $BaseUrl = Get-ProviderBaseUrl -ConfigText $configText -ProviderName $modelProvider
}
if ([string]::IsNullOrWhiteSpace($Model)) { throw "Model not provided and not found in Codex config." }
if ([string]::IsNullOrWhiteSpace($BaseUrl)) { throw "BaseUrl not provided and not found in Codex config." }

if ([string]::IsNullOrWhiteSpace($ModelId)) {
    $safeModel = ($Model -replace "[^A-Za-z0-9]+", "_").Trim("_").ToLowerInvariant()
    $ModelId = "model_codex_$safeModel"
}
if ([string]::IsNullOrWhiteSpace($Alias)) {
    $Alias = "Codex $Model"
}

$auth = Get-Content -LiteralPath $authPath -Raw | ConvertFrom-Json
$apiKey = [string]$auth.OPENAI_API_KEY
if ([string]::IsNullOrWhiteSpace($apiKey)) { throw "OPENAI_API_KEY not found in Codex auth.json." }

$backup = "$dbPath.codex-backup-$(Get-Date -Format 'yyyyMMdd-HHmmss')"
Copy-Item -LiteralPath $dbPath -Destination $backup
if (Test-Path -LiteralPath "$dbPath-wal") { Copy-Item -LiteralPath "$dbPath-wal" -Destination "$backup-wal" }
if (Test-Path -LiteralPath "$dbPath-shm") { Copy-Item -LiteralPath "$dbPath-shm" -Destination "$backup-shm" }

$encryptedSecret = ConvertTo-QoderEncryptedSecretJson -PlainText $apiKey -LocalStatePath $localStatePath

$now = [DateTimeOffset]::Now.ToUnixTimeMilliseconds()
$modelKey = "custom:$ModelId"
$record = [ordered]@{
    id = $ModelId
    provider = $Provider
    model = $Model
    alias = $Alias
    baseUrl = $BaseUrl
    visible = $true
    hasApiKey = $true
    is_vl = $false
    is_reasoning = $true
    max_input_tokens = 1000000
    byokTypeKey = $ByokTypeKey
    createTime = $now
}
$modelsJson = "[" + ($record | ConvertTo-Json -Compress) + "]"
$agentJson = @{
    "globalstate-selected-models" = @{
        chat = $modelKey
        assistant = $modelKey
        inline = "auto"
    }
} | ConvertTo-Json -Compress

$markerRaw = & $sqlite $dbPath "select value from ItemTable where key='__$__targetStorageMarker';"
if ($markerRaw) {
    $marker = $markerRaw | ConvertFrom-Json
}
else {
    $marker = [pscustomobject]@{}
}
$marker | Add-Member -NotePropertyName "aicoding.customModels" -NotePropertyValue 1 -Force
$marker | Add-Member -NotePropertyName "chat.modelConfig.assistant" -NotePropertyValue 0 -Force
$marker | Add-Member -NotePropertyName "chat.modelConfig.experts" -NotePropertyValue 0 -Force
$markerJson = $marker | ConvertTo-Json -Compress

$sql = @(
    "insert or replace into ItemTable(key,value) values('aicoding.customModels',$(SqlQuote $modelsJson));",
    "insert or replace into ItemTable(key,value) values('chat.modelConfig.assistant',$(SqlQuote $modelKey));",
    "insert or replace into ItemTable(key,value) values('chat.modelConfig.experts',$(SqlQuote $modelKey));",
    "insert or replace into ItemTable(key,value) values('aicoding.aicoding-agent',$(SqlQuote $agentJson));",
    "insert or replace into ItemTable(key,value) values('secret://aicoding.customModel.apiKey.$ModelId',$(SqlQuote $encryptedSecret.Json));",
    "insert or replace into ItemTable(key,value) values('__$__targetStorageMarker',$(SqlQuote $markerJson));"
) -join "`n"

$sqlPath = Join-Path $env:TEMP ("qoder-config-" + [Guid]::NewGuid().ToString("N") + ".sql")
Set-Content -LiteralPath $sqlPath -Value $sql -NoNewline -Encoding UTF8
try {
    & $sqlite $dbPath ".read '$sqlPath'"
}
finally {
    Remove-Item -LiteralPath $sqlPath -Force
}

$storedModels = & $sqlite $dbPath "select value from ItemTable where key='aicoding.customModels';"
$parsedModels = $storedModels | ConvertFrom-Json -NoEnumerate
if (($parsedModels -isnot [array]) -or $parsedModels.Count -lt 1 -or $parsedModels[0].id -ne $ModelId) {
    throw "Verification failed: aicoding.customModels was not written correctly."
}

$storedSecret = & $sqlite $dbPath "select value from ItemTable where key='secret://aicoding.customModel.apiKey.$ModelId';"
if (-not (Test-QoderSecret -SecretJson $storedSecret -Key $encryptedSecret.Key -ExpectedPlainText $apiKey)) {
    throw "Verification failed: encrypted API key cannot be decrypted to the Codex API key."
}

$assistant = & $sqlite $dbPath "select value from ItemTable where key='chat.modelConfig.assistant';"
if ($assistant -ne $modelKey) {
    throw "Verification failed: assistant model selection was not written."
}

Write-Output "Qoder custom model configured."
Write-Output "Model key: $modelKey"
Write-Output "Model: $Model"
Write-Output "Base URL: $BaseUrl"
Write-Output "Backup: $backup"
