functions/private/Invoke-KlippyJsonRpc.ps1

function Invoke-KlippyJsonRpc {
    <#
    .SYNOPSIS
        Invokes a JSON-RPC method on a Moonraker instance.

    .DESCRIPTION
        Sends a JSON-RPC 2.0 request to Moonraker and returns the result.
        Supports API key authentication, timeouts, and retries.
        Optionally normalizes response property names to PascalCase.

    .PARAMETER Printer
        The printer object (from Get-KlippyPrinter or Resolve-KlippyPrinterTarget).

    .PARAMETER Method
        The JSON-RPC method name (e.g., "printer.info", "server.info").

    .PARAMETER Params
        Optional parameters hashtable for the method.

    .PARAMETER Timeout
        Request timeout in seconds. Default is 30.

    .PARAMETER RetryCount
        Number of retries on failure. Default is 0.

    .PARAMETER RetryDelay
        Delay between retries in seconds. Default is 2.

    .PARAMETER NoNormalize
        Skip PascalCase normalization of response.

    .PARAMETER RawResponse
        Return the full response including jsonrpc envelope.

    .EXAMPLE
        $printer = Get-KlippyPrinter -PrinterName "voronv2"
        Invoke-KlippyJsonRpc -Printer $printer -Method "printer.info"

    .OUTPUTS
        PSCustomObject - The result from the JSON-RPC call.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$Printer,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Method,

        [Parameter()]
        [hashtable]$Params,

        [Parameter()]
        [ValidateRange(1, 600)]
        [int]$Timeout = 30,

        [Parameter()]
        [ValidateRange(0, 10)]
        [int]$RetryCount = 0,

        [Parameter()]
        [ValidateRange(1, 60)]
        [int]$RetryDelay = 2,

        [Parameter()]
        [switch]$NoNormalize,

        [Parameter()]
        [switch]$RawResponse
    )

    # Build the JSON-RPC request
    $requestId = [System.Random]::new().Next(1, 999999)
    $rpcRequest = @{
        jsonrpc = "2.0"
        method  = $Method
        id      = $requestId
    }

    if ($Params -and $Params.Count -gt 0) {
        $rpcRequest['params'] = $Params
    }

    $jsonBody = $rpcRequest | ConvertTo-Json -Depth 10 -Compress

    # Build headers
    $headers = @{
        'Content-Type' = 'application/json'
    }

    if ($Printer.ApiKey) {
        $headers['X-Api-Key'] = $Printer.ApiKey
    }

    # Build URI
    $baseUri = $Printer.Uri.TrimEnd('/')
    $uri = "$baseUri/printer/objects/query"

    # For non-query methods, use the appropriate endpoint
    # Moonraker accepts JSON-RPC at multiple endpoints or REST-style calls
    # We'll use REST-style for simplicity as it's more widely supported
    $uri = "$baseUri/api/$($Method -replace '\.', '/')"

    # Actually, let's use the direct endpoint mapping that Moonraker provides
    # Most methods map to: /method/path where method.path becomes /method/path
    $methodPath = $Method -replace '\.', '/'
    $uri = "$baseUri/$methodPath"

    $attempt = 0
    $maxAttempts = $RetryCount + 1
    $lastError = $null

    while ($attempt -lt $maxAttempts) {
        $attempt++

        try {
            Write-Verbose "[$($Printer.PrinterName)] Invoking $Method (attempt $attempt/$maxAttempts)"
            Write-Verbose "URI: $uri"

            $invokeParams = @{
                Uri         = $uri
                Method      = 'GET'
                Headers     = $headers
                TimeoutSec  = $Timeout
                ErrorAction = 'Stop'
            }

            # Use POST with body if we have params
            if ($Params -and $Params.Count -gt 0) {
                # For GET requests, convert params to query string
                $queryParams = @()
                foreach ($key in $Params.Keys) {
                    $value = $Params[$key]
                    if ($value -is [array]) {
                        $value = $value -join ','
                    }
                    $queryParams += "$key=$([System.Uri]::EscapeDataString($value.ToString()))"
                }
                if ($queryParams.Count -gt 0) {
                    $uri = "$uri`?$($queryParams -join '&')"
                    $invokeParams['Uri'] = $uri
                }
            }

            $response = Invoke-RestMethod @invokeParams

            # Handle Moonraker response format
            if ($RawResponse) {
                return $response
            }

            # Extract result (Moonraker wraps responses in {"result": ...})
            $result = if ($response.result) { $response.result } else { $response }

            # Normalize to PascalCase unless disabled
            if (-not $NoNormalize) {
                $result = ConvertTo-KlippyPascalCaseObject -InputObject $result
            }

            # Add printer context
            if ($result -is [PSCustomObject]) {
                $result | Add-Member -NotePropertyName '_PrinterId' -NotePropertyValue $Printer.Id -Force
                $result | Add-Member -NotePropertyName '_PrinterName' -NotePropertyValue $Printer.PrinterName -Force
            }

            return $result
        }
        catch {
            $lastError = $_
            Write-Verbose "[$($Printer.PrinterName)] Request failed: $($_.Exception.Message)"

            if ($attempt -lt $maxAttempts) {
                Write-Verbose "Retrying in $RetryDelay seconds..."
                Start-Sleep -Seconds $RetryDelay
            }
        }
    }

    # All retries exhausted
    $errorMessage = "Failed to invoke '$Method' on '$($Printer.PrinterName)' after $maxAttempts attempt(s): $($lastError.Exception.Message)"

    # Try to extract Moonraker error details
    if ($lastError.ErrorDetails.Message) {
        try {
            $errorDetails = $lastError.ErrorDetails.Message | ConvertFrom-Json
            if ($errorDetails.error.message) {
                $errorMessage = "[$($Printer.PrinterName)] $($errorDetails.error.message)"
            }
        }
        catch {
            # Ignore JSON parsing errors
        }
    }

    throw $errorMessage
}