EtherAssist.psm1

# Requires -Version 5.1

#region Internal Helper Functions
# Resolves a stable path under the current user's profile. Params: file name. Returns: full path.
function Get-EAUserConfigPath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$FileName
    )

    $home = [Environment]::GetFolderPath('UserProfile')
    if (-not $home) {
        throw "Unable to resolve the user profile directory."
    }
    return (Join-Path $home $FileName)
}

# Validates an absolute HTTP/HTTPS URL. Params: url. Returns: $true (or throws).
function Assert-EAHttpUrl {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Url
    )

    try {
        $u = [Uri]$Url
    }
    catch {
        throw "The URL '$Url' is not valid. Please provide a valid HTTP/HTTPS URL."
    }

    if (-not $u.IsAbsoluteUri -or $u.Scheme -notin @("http", "https")) {
        throw "The URL '$Url' is not valid. Please provide a valid HTTP/HTTPS URL."
    }

    return $true
}

# Joins base URL + endpoint and ensures the /api prefix is applied exactly once. Params: base, endpoint. Returns: full URI string.
function Join-EAApiUri {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$BaseUrl,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Endpoint
    )

    $base = $BaseUrl.TrimEnd('/')
    $path = if ($Endpoint.StartsWith('/')) { $Endpoint } else { "/$Endpoint" }
    if ($path -notmatch '^/api(/|$)') { $path = "/api$path" }
    if ($base -match '/api$' -and $path -match '^/api(/|$)') { $path = $path.Substring(4) }
    return "$base$path"
}

# Extracts HTTP status/body/retry metadata from Invoke-RestMethod errors. Params: error record. Returns: object with StatusCode, BodyText, RetryAfter.
function Get-EAHttpErrorInfo {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object]$ErrorRecord
    )

    $info = [ordered]@{
        StatusCode = $null
        BodyText = $null
        RetryAfter = $null
        Message = $null
    }

    try { $info.Message = $ErrorRecord.Exception.Message } catch {}

    $resp = $null
    try {
        if ($ErrorRecord.Exception -and ($ErrorRecord.Exception.PSObject.Properties.Name -contains "Response")) {
            $resp = $ErrorRecord.Exception.Response
        }
    } catch {}

    if ($resp) {
        try {
            if ($resp.PSObject.Properties.Name -contains "StatusCode") {
                $info.StatusCode = [int]$resp.StatusCode
            }
        } catch {}

        # Retry-After header (HttpWebResponse / HttpResponseMessage).
        try {
            if ($resp.PSObject.Properties.Name -contains "Headers" -and $resp.Headers) {
                $headers = $resp.Headers
                $ra = $null

                try { $ra = $headers["Retry-After"] } catch {}
                if (-not $ra -and ($headers.PSObject.Properties.Name -contains "RetryAfter") -and $headers.RetryAfter) {
                    if ($headers.RetryAfter.Delta) { $ra = [int]$headers.RetryAfter.Delta.TotalSeconds }
                    elseif ($headers.RetryAfter.Date) { $ra = $headers.RetryAfter.Date.ToString("o") }
                }

                if ($ra) { $info.RetryAfter = $ra }
            }
        } catch {}

        # Response body (HttpWebResponse stream / HttpResponseMessage content).
        try {
            if (($resp.PSObject.Properties.Name -contains "Content") -and $resp.Content) {
                $info.BodyText = $resp.Content.ReadAsStringAsync().GetAwaiter().GetResult()
            } elseif ($resp.PSObject.Methods.Name -contains "GetResponseStream") {
                $sr = [System.IO.StreamReader]::new($resp.GetResponseStream())
                try { $info.BodyText = $sr.ReadToEnd() } finally { $sr.Dispose() }
            }
        } catch {}
    }

    if (-not $info.BodyText) {
        try {
            if ($ErrorRecord.ErrorDetails -and $ErrorRecord.ErrorDetails.Message) {
                $info.BodyText = $ErrorRecord.ErrorDetails.Message
            }
        } catch {}
    }

    # Parse standard API error payloads for retryAfter/message.
    if ($info.BodyText) {
        try {
            $parsed = $info.BodyText | ConvertFrom-Json -ErrorAction Stop
            if (-not $info.RetryAfter -and $parsed.retryAfter) { $info.RetryAfter = [string]$parsed.retryAfter }
            if ($parsed.message) { $info.Message = [string]$parsed.message }
            if (-not $info.StatusCode -and $parsed.status) { $info.StatusCode = [int]$parsed.status }
        } catch {}
    }

    return [PSCustomObject]$info
}

# Extracts a human-friendly error string from an API response object. Params: response. Returns: string.
function Get-EAErrorText {
    [CmdletBinding()]
    param (
        [Parameter()]
        [object]$ResponseObject
    )

    if (-not $ResponseObject) { return "No response received from the API." }
    if ($ResponseObject.errorMessage) { return [string]$ResponseObject.errorMessage }
    if ($ResponseObject.message) { return [string]$ResponseObject.message }
    if ($ResponseObject.reason) {
        if ($ResponseObject.retryAfter) { return "$($ResponseObject.reason) (retryAfter: $($ResponseObject.retryAfter))" }
        return [string]$ResponseObject.reason
    }

    try { return ($ResponseObject | ConvertTo-Json -Depth 6) } catch { return "Unknown error" }
}

# Formats an EtherAssist API response into plain text / object / JSON. Params: response + input context. Returns: formatted output.
function Format-EAResponse {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [PSCustomObject]$ResponseObject,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$InputText,
        
        [ValidateNotNullOrEmpty()]
        [string]$InputType = "Question",
        
        [switch]$OutputAsJson,
        [switch]$OutputAsObject,
        [switch]$MuteQuestion,
        [switch]$MuteDateTime,
        [switch]$MuteAnswer
    )
    
    if (-not $ResponseObject.answer) {
        throw "Response object does not contain an answer"
    }
    
    try {
        if ($OutputAsJson)
        {
            $resultObject = [PSCustomObject]@{
                $InputType = $InputText
                Answer = $ResponseObject.answer
                TimeOfQuery = Get-Date -Format 'o'
                Metadata = @{
                    Status = $ResponseObject.status
                    StatusType = $ResponseObject.statusType
                    Model = $ResponseObject.model
                }
            }
            return $resultObject | ConvertTo-Json -Depth 10
        }
        elseif ($OutputAsObject)
        {
            $resultObject = [PSCustomObject]@{
                $InputType = $InputText
                Answer = $ResponseObject.answer
                TimeOfQuery = Get-Date
                Metadata = @{
                    Status = $ResponseObject.status
                    StatusType = $ResponseObject.statusType
                    Model = $ResponseObject.model
                }
            }
            return $resultObject
        }
        else
        {
            $responseMessage = @()
            if (-not $MuteQuestion)
            {
                $responseMessage += "$InputType`: $InputText"
            }
            if (-not $MuteDateTime)
            {
                $responseMessage += "Date/Time: $(Get-Date)"
            }
            
            $answerText = $ResponseObject.answer
            if (-not $MuteAnswer)
            {
                $responseMessage += "Answer: $answerText"
            }
            else
            {
                $responseMessage += $answerText
            }
            
            return $responseMessage -join "`n"
        }
    }
    catch {
        throw "Error formatting response: $_"
    }
}

# Saves the EtherAssist API key and base URL for subsequent requests. Params: ApiKey, ApiUrl. Returns: none.
function Set-EAApiConfig {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ApiKey,
        
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Assert-EAHttpUrl $_ })]
        [string]$ApiUrl = "https://api.etherassist.ai"
    )
    try {
        $KeyFilePath = Get-EAUserConfigPath -FileName "EtherAssistApiKey.xml"
        $secureApiKey = ConvertTo-SecureString $ApiKey -AsPlainText -Force
        $settings = @{
            ApiKey = $secureApiKey
            ApiUrl = $ApiUrl
        }
        $settings | Export-Clixml -Path $KeyFilePath -Force
        Write-Verbose "API configuration saved successfully. Using URL: $ApiUrl"
    }
    catch {
        Write-Error "Error storing API Key: $_"
    }
}

# Retrieves the saved API key/url (or env var overrides). Params: none. Returns: settings object.
function Get-EAApiConfig {
    [CmdletBinding()]
    param()
    
    $KeyFilePath = Get-EAUserConfigPath -FileName "EtherAssistApiKey.xml"
    if (Test-Path $KeyFilePath) {
        try {
            $settings = Import-Clixml -Path $KeyFilePath
            $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($settings.ApiKey)
            try {
                $settings.ApiKey = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
            }
            finally {
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
            }
            return $settings
        }
        catch {
            Write-Error "Error retrieving API Key: $_"
        }
    }
    else {
        $apiKey = $env:ETHERASSIST_API_KEY
        if ($apiKey) {
            return [PSCustomObject]@{
                ApiKey = $apiKey
                ApiUrl = if ($env:ETHERASSIST_API_URL) { $env:ETHERASSIST_API_URL } else { "https://api.etherassist.ai" }
            }
        }
        Write-Error "API Key and URL are not set. Use Set-EAApiConfig to configure (or set ETHERASSIST_API_KEY / ETHERASSIST_API_URL)."
    }
}

# Executes a POST request to the EtherAssist API with retry/backoff. Params: endpoint, body, options. Returns: parsed API response.
function Invoke-EtherAssistApi {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Endpoint,
        
        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object]$Body,
        
        [Parameter()]
        [ValidateRange(1, 300)]
        [int]$TimeoutSec = 30,
        
        [Parameter()]
        [ValidateRange(1, 10)]
        [int]$MaxRetries = 3,
        
        [Parameter()]
        [switch]$EnableLogging
    )
    
    $settings = Get-EAApiConfig
    if (-not $settings) {
        Write-Error "API settings are not set. Use Set-EAApiConfig to set the key and URL."
        return
    }

    $adv = Get-EAApiAdvancedConfig
    if (-not $adv) {
        $adv = [PSCustomObject]@{ DefaultTimeout = 30; MaxRetries = 3; EnableLogging = $false; ProxyUrl = $null }
    }
    if (-not $PSBoundParameters.ContainsKey('TimeoutSec')) {
        $TimeoutSec = if ($null -ne $adv.DefaultTimeout) { [int]$adv.DefaultTimeout } else { 30 }
    }
    if (-not $PSBoundParameters.ContainsKey('MaxRetries')) {
        $MaxRetries = if ($null -ne $adv.MaxRetries) { [int]$adv.MaxRetries } else { 3 }
    }
    $logEnabled = $EnableLogging.IsPresent -or ([bool]($adv.EnableLogging))
    $proxyUrl = if ($adv.ProxyUrl) { [string]$adv.ProxyUrl } else { $null }

    $uri = Join-EAApiUri -BaseUrl ([string]$settings.ApiUrl) -Endpoint $Endpoint
    $headers = @{
        Authorization = "Bearer $($settings.ApiKey)"
        'Content-Type' = 'application/json'
        Accept = 'application/json'
    }
    
    if ($logEnabled) {
        $safeHeaders = [ordered]@{}
        foreach ($k in $headers.Keys) { $safeHeaders[$k] = $headers[$k] }
        if ($safeHeaders.Authorization) { $safeHeaders.Authorization = "Bearer ****" }
        Write-Verbose "Request URI: $uri"
        Write-Verbose "Headers: $($safeHeaders | ConvertTo-Json)"
        Write-Verbose "Body: $($Body | ConvertTo-Json)"
    }
    
    $retryCount = 0
    $success = $false
    
    while (-not $success -and $retryCount -lt $MaxRetries) {
        try {
            $oldProgressPreference = $ProgressPreference
            $ProgressPreference = 'SilentlyContinue'
            try {
                if ($PSVersionTable.PSEdition -eq 'Desktop') {
                    # Ensure modern TLS for Windows PowerShell 5.1.
                    [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
                }

                $irmParams = @{
                    Uri = $uri
                    Method = 'Post'
                    Headers = $headers
                    Body = ($Body | ConvertTo-Json -Depth 10)
                    TimeoutSec = $TimeoutSec
                    ErrorAction = 'Stop'
                }
                if ($proxyUrl) { $irmParams.Proxy = $proxyUrl }

                $response = Invoke-RestMethod @irmParams
                $success = $true
                return $response
            }
            finally {
                $ProgressPreference = $oldProgressPreference
            }
        }
        catch {
            $retryCount++
            $errInfo = Get-EAHttpErrorInfo -ErrorRecord $_
            $statusCode = $errInfo.StatusCode

            $retryable = ($null -eq $statusCode) -or ($statusCode -in 408, 429, 500, 502, 503, 504)
            if (-not $retryable -or $retryCount -ge $MaxRetries) {
                if ($errInfo.BodyText) {
                    try {
                        $obj = $errInfo.BodyText | ConvertFrom-Json -ErrorAction Stop
                        if (-not $obj.success) { $obj | Add-Member NoteProperty success $false -Force }
                        if (-not $obj.status -and $statusCode) { $obj | Add-Member NoteProperty status $statusCode -Force }
                        return $obj
                    } catch {
                        return [PSCustomObject]@{ success = $false; status = (if ($null -ne $statusCode) { [int]$statusCode } else { 500 }); message = $errInfo.BodyText }
                    }
                }
                return [PSCustomObject]@{ success = $false; status = (if ($null -ne $statusCode) { [int]$statusCode } else { 500 }); message = (if ($errInfo.Message) { $errInfo.Message } else { "Request failed" }) }
            }

            $sleepSec = $null
            if ($statusCode -eq 429 -and $errInfo.RetryAfter) {
                if ($errInfo.RetryAfter -is [int]) {
                    $sleepSec = [int]$errInfo.RetryAfter
                } else {
                    try {
                        $retryAt = [datetime]::Parse([string]$errInfo.RetryAfter)
                        $sleepSec = [math]::Ceiling(($retryAt.ToUniversalTime() - (Get-Date).ToUniversalTime()).TotalSeconds)
                    } catch {}
                }
            }
            if (-not $sleepSec) {
                $sleepSec = [math]::Min(30, (2 * [math]::Pow(2, [math]::Min($retryCount, 4))) + (Get-Random -Minimum 0 -Maximum 3))
            }
            if ($logEnabled) { Write-Verbose "Retrying in $sleepSec seconds (attempt $($retryCount + 1) of $MaxRetries)..." }
            Start-Sleep -Seconds ([math]::Max(1, $sleepSec))
        }
    }
}

# Sends a one-off question to the EtherAssist API. Params: Question + output switches. Returns: formatted output.
function Send-EARequest {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Question,
        [switch]$UseAdvancedModel,
        [switch]$MuteQuestion,
        [switch]$MuteDateTime,
        [switch]$MuteAnswer,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $Question
        useAdvancedModel = $UseAdvancedModel.IsPresent
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/question" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $Question -InputType "Question" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject `
            -MuteQuestion:$MuteQuestion -MuteDateTime:$MuteDateTime -MuteAnswer:$MuteAnswer
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Sends a basic completion request (no topics/embeddings) via EtherAssist. Params: Question + optional SystemPrompt/Model. Returns: formatted output.
function Send-EACompletion {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Question,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$SystemPrompt,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Model,

        [switch]$UseAdvancedModel,
        [switch]$MuteQuestion,
        [switch]$MuteDateTime,
        [switch]$MuteAnswer,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )

    $body = @{
        question = $Question
        useAdvancedModel = $UseAdvancedModel.IsPresent
    }
    if ($SystemPrompt) { $body.systemPrompt = $SystemPrompt }
    if ($Model) { $body.model = $Model }

    $responseObject = Invoke-EtherAssistApi -Endpoint "/completions" -Body $body

    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $Question -InputType "Question" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject `
            -MuteQuestion:$MuteQuestion -MuteDateTime:$MuteDateTime -MuteAnswer:$MuteAnswer
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Prompts interactively for a question and sends it to EtherAssist. Params: switches. Returns: formatted output.
function Invoke-EAQuery {
    [CmdletBinding()]
    param (
        [switch]$UseAdvancedModel,
        [switch]$MuteQuestion,
        [switch]$MuteDateTime,
        [switch]$MuteAnswer
    )
    
    try {
        $question = Read-Host "Please enter the question for EtherAssist"
        Send-EARequest -Question $question -UseAdvancedModel:$UseAdvancedModel -MuteQuestion:$MuteQuestion -MuteDateTime:$MuteDateTime -MuteAnswer:$MuteAnswer
    }
    catch {
        Write-Error "An error occurred while invoking EtherAssist query: $_"
    }
}

# Sends a reflective (cognitive) question to EtherAssist. Params: Question + output switches. Returns: formatted output.
function Send-EAReflectiveRequest {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Question,
        [switch]$MuteQuestion,
        [switch]$MuteDateTime,
        [switch]$MuteAnswer,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )

    $body = @{
        question = $Question
    }

    $responseObject = Invoke-EtherAssistApi -Endpoint "/reflective" -Body $body

    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $Question -InputType "Question" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject `
            -MuteQuestion:$MuteQuestion -MuteDateTime:$MuteDateTime -MuteAnswer:$MuteAnswer
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Performs a web search via EtherAssist and returns a summarized answer. Params: Query + output switches. Returns: formatted output.
function Send-EAWebSearch {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Query,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject,
        [switch]$MuteQuestion,
        [switch]$MuteDateTime,
        [switch]$MuteAnswer
    )
    
    $body = @{
        question = $Query
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/web-search" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $Query -InputType "SearchQuery" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject `
            -MuteQuestion:$MuteQuestion -MuteDateTime:$MuteDateTime -MuteAnswer:$MuteAnswer
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Converts Batch script text to PowerShell via EtherAssist. Params: BatchCode + output switches. Returns: formatted output.
function Convert-EABatchToPs {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$BatchCode,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $BatchCode
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/gen/convert-batch-to-ps" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $BatchCode -InputType "BatchScript" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Generates a knowledge article for a topic via EtherAssist. Params: Topic + output switches. Returns: formatted output.
function New-EAKnowledgeArticle {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Topic,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject,
        [switch]$MuteQuestion,
        [switch]$MuteDateTime,
        [switch]$MuteAnswer
    )
    
    $body = @{
        question = $Topic
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/gen/create-knowledge-article" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $Topic -InputType "Topic" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject `
            -MuteQuestion:$MuteQuestion -MuteDateTime:$MuteDateTime -MuteAnswer:$MuteAnswer
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Persists advanced request defaults (timeout/retry/logging/proxy). Params: settings. Returns: none.
function Set-EAApiAdvancedConfig {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateRange(1, 300)]
        [int]$DefaultTimeout = 30,
        
        [Parameter()]
        [ValidateRange(1, 10)]
        [int]$MaxRetries = 3,
        
        [Parameter()]
        [switch]$EnableLogging,
        
        [Parameter()]
        [ValidateScript({
            if ([string]::IsNullOrEmpty($_)) { return $true }
            return (Assert-EAHttpUrl $_)
        })]
        [string]$ProxyUrl
    )
    
    try {
        $ConfigFilePath = Get-EAUserConfigPath -FileName "EtherAssistConfig.xml"
        
        $config = @{
            DefaultTimeout = $DefaultTimeout
            MaxRetries = $MaxRetries
            EnableLogging = $EnableLogging.IsPresent
            ProxyUrl = $ProxyUrl
            LastModified = Get-Date
        }
        
        # Encrypt sensitive data
        if ($ProxyUrl) {
            $secureProxyUrl = ConvertTo-SecureString $ProxyUrl -AsPlainText -Force
            $config.ProxyUrl = $secureProxyUrl
        }
        
        $config | Export-Clixml -Path $ConfigFilePath -Force
        Write-Verbose "Advanced configuration saved successfully"
    }
    catch {
        Write-Error "Error saving advanced configuration: $_"
    }
}

# Loads advanced request defaults (or returns built-in defaults). Params: none. Returns: config object.
function Get-EAApiAdvancedConfig {
    [CmdletBinding()]
    param()
    
    $ConfigFilePath = Get-EAUserConfigPath -FileName "EtherAssistConfig.xml"
    
    if (Test-Path $ConfigFilePath) {
        try {
            $config = Import-Clixml -Path $ConfigFilePath
            
            # Decrypt sensitive data
            if ($config.ProxyUrl -is [System.Security.SecureString]) {
                $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($config.ProxyUrl)
                try {
                    $config.ProxyUrl = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
                }
                finally {
                    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
                }
            }
            
            return [PSCustomObject]$config
        }
        catch {
            Write-Error "Error retrieving advanced configuration: $_"
        }
    }
    else {
        Write-Verbose "No advanced configuration found. Using defaults."
        return [PSCustomObject]@{
            DefaultTimeout = 30
            MaxRetries = 3
            EnableLogging = $false
            ProxyUrl = $null
            LastModified = $null
        }
    }
}

# Generates a short title from input text via EtherAssist. Params: Text + output switches. Returns: formatted output.
function Get-EATitle {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Text,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $Text
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/utils/title" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $Text -InputType "Text" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Summarizes input text via EtherAssist. Params: Text + output switches. Returns: formatted output.
function Get-EATextSummary {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Text,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $Text
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/utils/Summarize" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $Text -InputType "Text" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Summarizes Azure billing data into a structured report. Params: bill text. Returns: formatted output.
function Get-EAAzureBillSummary {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$BillText,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )

    $body = @{
        question = $BillText
    }

    $responseObject = Invoke-EtherAssistApi -Endpoint "/utils/SummarizeBill" -Body $body

    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $BillText -InputType "Bill" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Analyzes log content via EtherAssist. Params: LogContent + output switches. Returns: formatted output.
function Get-EALogAnalysis {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$LogContent,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $LogContent
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/gen/log-analyze" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $LogContent -InputType "LogContent" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Explains an error code/string and suggests resolutions via EtherAssist. Params: ErrorCode + output switches. Returns: formatted output.
function Get-EAErrorCodeDescription {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ErrorCode,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $ErrorCode
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/gen/errorcode" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $ErrorCode -InputType "ErrorCode" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Converts VBScript text to PowerShell via EtherAssist. Params: VbsCode + output switches. Returns: formatted output.
function Convert-EAVbsToPs {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$VbsCode,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $VbsCode
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/gen/convert-vbs-to-ps" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $VbsCode -InputType "VbsScript" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Provides an application description via EtherAssist. Params: AppName + output switches. Returns: formatted output.
function Get-EAAppDescription {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$AppName,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $AppName
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/apps/app-description" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $AppName -InputType "AppName" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Provides silent installer arguments for an app via EtherAssist. Params: AppName + output switches. Returns: formatted output.
function Get-EAAppInstallerArgs {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$AppName,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $AppName
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/apps/installer-args" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $AppName -InputType "AppName" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Analyzes MSIX AppxManifest.xml content (optionally App Attach) via EtherAssist. Params: ManifestContent + switches. Returns: formatted output.
function Get-EAMsixAnalysis {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$ManifestContent,
        [switch]$AppAttachAnalysis,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $ManifestContent
    }
    
    $endpoint = if ($AppAttachAnalysis) { "/apps/msix-app-attach-analysis" } else { "/apps/msix-analysis" }
    $responseObject = Invoke-EtherAssistApi -Endpoint $endpoint -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $ManifestContent -InputType "MsixManifest" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Analyzes Entra sign-in/audit log data for threats via EtherAssist. Params: LogData + output switches. Returns: formatted output.
function Get-EAEntraThreatAnalysis {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$LogData,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $LogData
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/entra-threat-detection" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $LogData -InputType "EntraLogs" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

# Analyzes PCAP content for suspicious activity via EtherAssist. Params: PcapContent + output switches. Returns: formatted output.
function Get-EAPcapAnalysis {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$PcapContent,
        [switch]$OutputAsJson,
        [switch]$OutputAsObject
    )
    
    $body = @{
        question = $PcapContent
    }
    
    $responseObject = Invoke-EtherAssistApi -Endpoint "/utils/pcap-analyze" -Body $body
    
    if ($responseObject.success -eq $true) {
        Format-EAResponse -ResponseObject $responseObject -InputText $PcapContent -InputType "PcapData" `
            -OutputAsJson:$OutputAsJson -OutputAsObject:$OutputAsObject
    }
    else {
        Write-Error "API request was not successful: $(Get-EAErrorText -ResponseObject $responseObject)"
    }
}

Export-ModuleMember -Function Set-EAApiConfig, Get-EAApiConfig, Send-EARequest, Send-EACompletion, Send-EAReflectiveRequest, Invoke-EAQuery, Send-EAWebSearch, Convert-EABatchToPs, New-EAKnowledgeArticle, Set-EAApiAdvancedConfig, Get-EAApiAdvancedConfig, Format-EAResponse, Get-EATitle, Get-EATextSummary, Get-EAAzureBillSummary, Get-EALogAnalysis, Get-EAErrorCodeDescription, Convert-EAVbsToPs, Get-EAAppDescription, Get-EAAppInstallerArgs, Get-EAMsixAnalysis, Get-EAEntraThreatAnalysis, Get-EAPcapAnalysis