functions/private/Invoke-KlippyHttpRequest.ps1

function Invoke-KlippyHttpRequest {
    <#
    .SYNOPSIS
        Performs HTTP requests to Moonraker for file operations.

    .DESCRIPTION
        Handles GET/POST/DELETE requests for file uploads, downloads, and management.
        Supports streaming downloads, multipart uploads, and authentication.

    .PARAMETER Printer
        The printer object.

    .PARAMETER Endpoint
        The API endpoint path (e.g., "/server/files/gcodes/file.gcode").

    .PARAMETER Method
        HTTP method. Default is GET.

    .PARAMETER Body
        Request body for POST requests.

    .PARAMETER OutFile
        Path to save downloaded file (for streaming downloads).

    .PARAMETER InFile
        Path to file to upload.

    .PARAMETER ContentType
        Content type for the request.

    .PARAMETER QueryParameters
        Hashtable of query parameters.

    .PARAMETER Timeout
        Request timeout in seconds. Default is 300 (5 minutes for large files).

    .PARAMETER NoNormalize
        Skip PascalCase normalization of JSON response.

    .EXAMPLE
        Invoke-KlippyHttpRequest -Printer $printer -Endpoint "/server/files/list" -QueryParameters @{root="gcodes"}

    .EXAMPLE
        Invoke-KlippyHttpRequest -Printer $printer -Endpoint "/server/files/gcodes/test.gcode" -OutFile "C:\temp\test.gcode"

    .OUTPUTS
        PSCustomObject or file path for downloads.
    #>

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

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

        [Parameter()]
        [ValidateSet('GET', 'POST', 'DELETE')]
        [string]$Method = 'GET',

        [Parameter()]
        $Body,

        [Parameter()]
        [string]$OutFile,

        [Parameter()]
        [string]$InFile,

        [Parameter()]
        [string]$ContentType,

        [Parameter()]
        [hashtable]$QueryParameters,

        [Parameter()]
        [ValidateRange(1, 3600)]
        [int]$Timeout = 300,

        [Parameter()]
        [switch]$NoNormalize
    )

    # Build URI
    $baseUri = $Printer.Uri.TrimEnd('/')
    $endpoint = $Endpoint.TrimStart('/')
    $uri = "$baseUri/$endpoint"

    # Add query parameters
    if ($QueryParameters -and $QueryParameters.Count -gt 0) {
        $queryParts = @()
        foreach ($key in $QueryParameters.Keys) {
            $value = $QueryParameters[$key]
            if ($null -ne $value) {
                $queryParts += "$key=$([System.Uri]::EscapeDataString($value.ToString()))"
            }
        }
        if ($queryParts.Count -gt 0) {
            $uri = "$uri`?$($queryParts -join '&')"
        }
    }

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

    Write-Verbose "[$($Printer.PrinterName)] HTTP $Method $uri"

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

        # Handle file upload
        if ($InFile) {
            if (-not (Test-Path -Path $InFile -PathType Leaf)) {
                throw "File not found: $InFile"
            }

            $fileName = Split-Path -Path $InFile -Leaf

            # Use multipart form data for file uploads
            $fileBytes = [System.IO.File]::ReadAllBytes($InFile)
            $boundary = [System.Guid]::NewGuid().ToString()

            $bodyLines = @(
                "--$boundary",
                "Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"",
                "Content-Type: application/octet-stream",
                "",
                [System.Text.Encoding]::GetEncoding("ISO-8859-1").GetString($fileBytes),
                "--$boundary--"
            )

            $bodyContent = $bodyLines -join "`r`n"
            $invokeParams['Body'] = $bodyContent
            $invokeParams['ContentType'] = "multipart/form-data; boundary=$boundary"
        }
        # Handle JSON body
        elseif ($Body) {
            if ($Body -is [hashtable] -or $Body -is [PSCustomObject]) {
                $invokeParams['Body'] = ($Body | ConvertTo-Json -Depth 10 -Compress)
                $invokeParams['ContentType'] = 'application/json'
            }
            else {
                $invokeParams['Body'] = $Body
                if ($ContentType) {
                    $invokeParams['ContentType'] = $ContentType
                }
            }
        }

        # Handle file download
        if ($OutFile) {
            # Ensure directory exists
            $outDir = Split-Path -Path $OutFile -Parent
            if ($outDir -and -not (Test-Path -Path $outDir -PathType Container)) {
                $null = New-Item -Path $outDir -ItemType Directory -Force
            }

            $invokeParams['OutFile'] = $OutFile
            Invoke-RestMethod @invokeParams

            Write-Verbose "File saved to: $OutFile"
            return $OutFile
        }

        # Standard request
        $response = Invoke-RestMethod @invokeParams

        # Extract result if wrapped
        $result = if ($response.result) { $response.result } else { $response }

        # Normalize unless disabled
        if (-not $NoNormalize -and ($result -is [PSCustomObject] -or $result -is [array])) {
            $result = ConvertTo-KlippyPascalCaseObject -InputObject $result
        }

        return $result
    }
    catch {
        $errorMessage = "HTTP request failed: $($_.Exception.Message)"

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

        throw $errorMessage
    }
}