Invoke-AICli.ps1

function Invoke-AICli {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('ClaudeCode', 'GitHubCopilot', 'OpenAI')]
        [string]$Provider,

        [Parameter(Mandatory = $true)]
        [string]$Prompt,

        [Parameter()]
        [string[]]$SkillFiles,

        [Parameter()]
        [string[]]$AllowedTools,

        [Parameter()]
        [string]$Model,

        [Parameter()]
        [string]$ApiKey,

        [Parameter()]
        [string]$WorkingDirectory,

        [Parameter()]
        [string[]]$McpConfig,

        [Parameter()]
        [string]$PermissionMode,

        [Parameter()]
        [hashtable]$EnvironmentVars,

        [Parameter()]
        [int]$TimeoutMinutes = 30,

        [Parameter()]
        [switch]$EnableStream,

        [Parameter()]
        [switch]$ShowRawOutput,

        [Parameter()]
        [string]$RawOutputPath
    )

    $resolvedWorkingDirectory = Resolve-AICliWorkingDirectory -WorkingDirectory $WorkingDirectory

    try {
        $cliArgs = Build-AICLiArguments -Provider $Provider -Prompt $Prompt -SkillFiles $SkillFiles `
            -AllowedTools $AllowedTools -Model $Model -WorkingDirectory $resolvedWorkingDirectory `
            -McpConfig $McpConfig -PermissionMode $PermissionMode

        $envBackup = @{}
        if ($ApiKey) {
            switch ($Provider) {
                'ClaudeCode' {
                    $envBackup['ANTHROPIC_API_KEY'] = $env:ANTHROPIC_API_KEY
                    $env:ANTHROPIC_API_KEY = $ApiKey
                }
                'GitHubCopilot' {
                    $envBackup['COPILOT_GITHUB_TOKEN'] = $env:COPILOT_GITHUB_TOKEN
                    $env:COPILOT_GITHUB_TOKEN = $ApiKey
                }
                'OpenAI' {
                    $envBackup['OPENAI_API_KEY'] = $env:OPENAI_API_KEY
                    $env:OPENAI_API_KEY = $ApiKey
                }
            }
        }

        if ($EnvironmentVars) {
            foreach ($key in $EnvironmentVars.Keys) {
                $envBackup[$key] = [System.Environment]::GetEnvironmentVariable($key)
                [System.Environment]::SetEnvironmentVariable($key, $EnvironmentVars[$key])
            }
        }

        $executable = Get-AICliExecutable -Provider $Provider
        $cliArgsForLog = Format-AICliArgumentsForLog -Arguments $cliArgs
        Write-Host "Invoking $Provider CLI: $executable $cliArgsForLog"

        if ($EnableStream) {
            $result = Invoke-AICliStreaming -Executable $executable -Arguments $cliArgs -TimeoutMinutes $TimeoutMinutes -WorkingDirectory $resolvedWorkingDirectory
        }
        else {
            $result = Invoke-AICliBuffered -Executable $executable -Arguments $cliArgs -TimeoutMinutes $TimeoutMinutes -WorkingDirectory $resolvedWorkingDirectory
        }

        if ($ShowRawOutput -and $result.Output) {
            Write-Host "##[group]AI CLI Raw Output"
            Write-Host $result.Output
            Write-Host "##[endgroup]"
        }

        if ($RawOutputPath -and $result.Output) {
            $result.Output | Set-Content -Path $RawOutputPath -Encoding UTF8
            Write-Host "Raw output saved to: $RawOutputPath"
        }

        return $result
    }
    finally {
        foreach ($key in $envBackup.Keys) {
            if ($null -eq $envBackup[$key]) {
                [System.Environment]::SetEnvironmentVariable($key, $null)
            }
            else {
                [System.Environment]::SetEnvironmentVariable($key, $envBackup[$key])
            }
        }
    }
}

function Build-AICLiArguments {
    [CmdletBinding()]
    param(
        [string]$Provider,
        [string]$Prompt,
        [string[]]$SkillFiles,
        [string[]]$AllowedTools,
        [string]$Model,
        [string]$WorkingDirectory,
        [string[]]$McpConfig,
        [string]$PermissionMode
    )

    $cliArgs = @()

    switch ($Provider) {
        'ClaudeCode' {
            $cliArgs += '--print'
            if ($AllowedTools) {
                $cliArgs += '--allowedTools'
                $cliArgs += ($AllowedTools -join ',')
            }
            else {
                $cliArgs += '--allowedTools'
                $cliArgs += '""'
            }
            if ($WorkingDirectory) {
                $cliArgs += '--add-dir'
                $cliArgs += $WorkingDirectory
            }
            if ($McpConfig) {
                foreach ($mcpConfigEntry in $McpConfig) {
                    if ($mcpConfigEntry) {
                        $cliArgs += '--mcp-config'
                        $cliArgs += $mcpConfigEntry
                    }
                }
            }
            if ($PermissionMode) {
                $cliArgs += '--permission-mode'
                $cliArgs += $PermissionMode
            }
            if ($SkillFiles) {
                foreach ($skillFile in $SkillFiles) {
                    if (Test-Path $skillFile) {
                        $cliArgs += '--append-system-prompt-file'
                        $cliArgs += $skillFile
                    }
                    else {
                        Write-Warning "Skill file not found: $skillFile"
                    }
                }
            }
            if ($Model) {
                $cliArgs += '--model'
                $cliArgs += $Model
            }
            $cliArgs += '--'
            $cliArgs += $Prompt
        }
        'GitHubCopilot' {
            $cliArgs += '-p'
            $cliArgs += $Prompt
            $cliArgs += '-s'
            $cliArgs += '--no-ask-user'
            if ($WorkingDirectory) {
                $cliArgs += '-C'
                $cliArgs += $WorkingDirectory
            }
            if ($AllowedTools) {
                $cliArgs += '--allow-tool'
                $cliArgs += ($AllowedTools -join ',')
            }
            else {
                $cliArgs += '--allow-all-tools'
            }
            if ($McpConfig) {
                foreach ($mcpConfigEntry in $McpConfig) {
                    if ($mcpConfigEntry) {
                        $cliArgs += '--additional-mcp-config'
                        $cliArgs += $mcpConfigEntry
                    }
                }
            }
            if ($SkillFiles) {
                foreach ($skillFile in $SkillFiles) {
                    if (Test-Path $skillFile) {
                        $cliArgs += '--attachment'
                        $cliArgs += $skillFile
                    }
                    else {
                        Write-Warning "Skill file not found: $skillFile"
                    }
                }
            }
            if ($Model) {
                $cliArgs += '--model'
                $cliArgs += $Model
            }
            $cliArgs += '--secret-env-vars'
            $cliArgs += 'COPILOT_GITHUB_TOKEN,GH_TOKEN,GITHUB_TOKEN,SYSTEM_ACCESSTOKEN'
        }
        'OpenAI' {
            $cliArgs += 'chat'
            if ($Model) {
                $cliArgs += '--model'
                $cliArgs += $Model
            }
            $cliArgs += '--message'
            $cliArgs += $Prompt
        }
    }

    return $cliArgs
}

function Resolve-AICliWorkingDirectory {
    [CmdletBinding()]
    param(
        [string]$WorkingDirectory
    )

    if (-not $WorkingDirectory) {
        return ''
    }

    try {
        $item = Get-Item -LiteralPath $WorkingDirectory -ErrorAction Stop
        if ($item.PSIsContainer) {
            return $item.FullName
        }

        Write-Warning "AI CLI working directory is not a directory: $WorkingDirectory"
    }
    catch {
        Write-Warning "AI CLI working directory is not accessible: $WorkingDirectory. $($_.Exception.Message)"
    }

    return ''
}

function Format-AICliArgumentsForLog {
    [CmdletBinding()]
    param(
        [string[]]$Arguments
    )

    if (-not $Arguments) {
        return ''
    }

    $promptSeparatorIndex = [Array]::IndexOf($Arguments, '--')
    if ($promptSeparatorIndex -lt 0 -or $promptSeparatorIndex -eq ($Arguments.Count - 1)) {
        return ($Arguments -join ' ')
    }

    $argumentsBeforePrompt = @()
    if ($promptSeparatorIndex -gt 0) {
        $argumentsBeforePrompt = $Arguments[0..$promptSeparatorIndex]
    }
    else {
        $argumentsBeforePrompt = @('--')
    }

    $promptLength = ($Arguments[($promptSeparatorIndex + 1)..($Arguments.Count - 1)] -join ' ').Length
    return "$(($argumentsBeforePrompt -join ' ')) <prompt omitted; $promptLength chars>"
}

function Get-AICliExecutable {
    [CmdletBinding()]
    param(
        [string]$Provider
    )

    switch ($Provider) {
        'ClaudeCode' { return Get-ExecutablePath -ExecutableName 'claude' -NativeInstallSubPath '.local\bin\claude.exe' }
        'GitHubCopilot' { return 'copilot' }
        'OpenAI' { return 'openai' }
    }
}

function Get-ExecutablePath {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ExecutableName,

        [Parameter(Mandatory = $true)]
        [string]$NativeInstallSubPath
    )

    $command = Get-Command $ExecutableName -ErrorAction SilentlyContinue
    if ($command) {
        return $command.Source
    }

    $nativeInstallPath = Join-Path $HOME $NativeInstallSubPath
    if (Test-Path $nativeInstallPath) {
        return $nativeInstallPath
    }

    return $ExecutableName
}

function Invoke-AICliBuffered {
    [CmdletBinding()]
    param(
        [string]$Executable,
        [string[]]$Arguments,
        [int]$TimeoutMinutes,
        [string]$WorkingDirectory
    )

    $psi = [System.Diagnostics.ProcessStartInfo]::new()
    $psi.FileName = $Executable
    $psi.Arguments = Join-ProcessArguments -Arguments $Arguments
    if ($WorkingDirectory) {
        $psi.WorkingDirectory = $WorkingDirectory
    }
    $psi.UseShellExecute = $false
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.CreateNoWindow = $true

    $process = [System.Diagnostics.Process]::new()
    $process.StartInfo = $psi

    try {
        $process.Start() | Out-Null

        $outputTask = $process.StandardOutput.ReadToEndAsync()
        $errorTask = $process.StandardError.ReadToEndAsync()

        $timeoutMs = $TimeoutMinutes * 60 * 1000
        if (-not $process.WaitForExit($timeoutMs)) {
            $process.Kill()
            Write-Warning "AI CLI process timed out after $TimeoutMinutes minutes"
            return @{
                ExitCode = -1
                Output   = "Process timed out after $TimeoutMinutes minutes"
                Success  = $false
            }
        }

        $output = $outputTask.GetAwaiter().GetResult()
        $errorOutput = $errorTask.GetAwaiter().GetResult()

        if ($errorOutput) {
            Write-Warning "AI CLI stderr: $errorOutput"
        }

        return @{
            ExitCode = $process.ExitCode
            Output   = $output
            Success  = ($process.ExitCode -eq 0)
        }
    }
    finally {
        if ($process) { $process.Dispose() }
    }
}

function Join-ProcessArguments {
    [CmdletBinding()]
    param(
        [string[]]$Arguments
    )

    if (-not $Arguments) {
        return ''
    }

    return (($Arguments | ForEach-Object { ConvertTo-ProcessArgument -Argument $_ }) -join ' ')
}

function ConvertTo-ProcessArgument {
    [CmdletBinding()]
    param(
        [AllowEmptyString()]
        [string]$Argument
    )

    if ($null -eq $Argument) {
        return '""'
    }

    if ($Argument -notmatch '[\s"]') {
        return $Argument
    }

    $result = [System.Text.StringBuilder]::new()
    $result.Append('"') | Out-Null
    $backslashCount = 0

    foreach ($character in $Argument.ToCharArray()) {
        if ($character -eq '\') {
            $backslashCount++
            continue
        }

        if ($character -eq '"') {
            $result.Append('\' * (($backslashCount * 2) + 1)) | Out-Null
            $result.Append('"') | Out-Null
            $backslashCount = 0
            continue
        }

        if ($backslashCount -gt 0) {
            $result.Append('\' * $backslashCount) | Out-Null
            $backslashCount = 0
        }

        $result.Append($character) | Out-Null
    }

    if ($backslashCount -gt 0) {
        $result.Append('\' * ($backslashCount * 2)) | Out-Null
    }

    $result.Append('"') | Out-Null
    return $result.ToString()
}

function Invoke-AICliStreaming {
    [CmdletBinding()]
    param(
        [string]$Executable,
        [string[]]$Arguments,
        [int]$TimeoutMinutes,
        [string]$WorkingDirectory
    )

    $outputBuilder = [System.Text.StringBuilder]::new()

    $psi = [System.Diagnostics.ProcessStartInfo]::new()
    $psi.FileName = $Executable
    $psi.Arguments = Join-ProcessArguments -Arguments $Arguments
    if ($WorkingDirectory) {
        $psi.WorkingDirectory = $WorkingDirectory
    }
    $psi.UseShellExecute = $false
    $psi.RedirectStandardOutput = $true
    $psi.RedirectStandardError = $true
    $psi.CreateNoWindow = $true

    $process = [System.Diagnostics.Process]::new()
    $process.StartInfo = $psi

    Write-Host "##[group]AI CLI Streaming Output"

    try {
        $process.Start() | Out-Null

        $deadline = [DateTime]::Now.AddMinutes($TimeoutMinutes)

        while (-not $process.StandardOutput.EndOfStream) {
            if ([DateTime]::Now -gt $deadline) {
                $process.Kill()
                Write-Host "##[endgroup]"
                Write-Warning "AI CLI process timed out after $TimeoutMinutes minutes"
                return @{
                    ExitCode = -1
                    Output   = $outputBuilder.ToString()
                    Success  = $false
                }
            }

            $line = $process.StandardOutput.ReadLine()
            if ($null -ne $line) {
                Write-Host $line
                $outputBuilder.AppendLine($line) | Out-Null
            }
        }

        $process.WaitForExit()
        Write-Host "##[endgroup]"

        $errorOutput = $process.StandardError.ReadToEnd()
        if ($errorOutput) {
            Write-Warning "AI CLI stderr: $errorOutput"
        }

        return @{
            ExitCode = $process.ExitCode
            Output   = $outputBuilder.ToString()
            Success  = ($process.ExitCode -eq 0)
        }
    }
    catch {
        Write-Host "##[endgroup]"
        throw
    }
}