param(
    [Parameter(Mandatory = $true)][string]$ConfigFile,
    [string]$Server = 'http://127.0.0.1:8189'
)

$ErrorActionPreference = 'Stop'
if (-not (Test-Path -LiteralPath $ConfigFile -PathType Leaf)) {
    throw "R2V config not found: $ConfigFile"
}
$config = Get-Content -LiteralPath $ConfigFile -Raw -Encoding UTF8 | ConvertFrom-Json
$width = [int]$config.width
$height = [int]$config.height
$steps = [int]$config.steps
$seed = [int64]$config.seed
$duration = [double]$config.duration
$promptText = [string]$config.prompt
$refImageSize = [string]$config.ref_image_size
$outputPrefix = [string]$config.output_prefix
$latentPrefix = [string]$config.latent_prefix

if ($width -le 0 -or $height -le 0 -or $width % 32 -ne 0 -or $height % 32 -ne 0) {
    throw 'Width and height must be positive multiples of 32.'
}
if ($steps -lt 8 -or $steps -gt 30) {
    throw 'R2V steps must be between 8 and 30.'
}
if ($duration -le 0 -or $duration -gt 15) {
    throw 'R2V duration must be in (0, 15].'
}
if ([string]::IsNullOrWhiteSpace($promptText)) {
    throw 'R2V prompt must not be empty.'
}
if ($refImageSize -notin @('match', 'max')) {
    throw 'ref_image_size must be match or max.'
}
if ($config.ref_images.Count -gt 9 -or $config.ref_videos.Count -gt 3 -or $config.ref_audios.Count -gt 3) {
    throw 'R2V reference count exceeds the MiniMax H3 limits.'
}
if (($config.ref_images.Count + $config.ref_videos.Count + $config.ref_audios.Count) -eq 0) {
    throw 'R2V requires at least one image, video, or audio reference.'
}

$frameLength = [math]::Max(5, [math]::Round($duration * 24))
$frameLength += (5 - ($frameLength % 17) + 17) % 17

function Invoke-ComfyPrompt {
    param(
        [Parameter(Mandatory = $true)]$Prompt,
        [Parameter(Mandatory = $true)][string]$Stage,
        [int]$TimeoutSeconds = 7200
    )
    $body = @{
        prompt = $Prompt
        client_id = [guid]::NewGuid().ToString()
    } | ConvertTo-Json -Depth 40 -Compress
    $queued = Invoke-RestMethod -Uri "$Server/prompt" -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 30
    if (-not $queued.prompt_id) {
        throw "ComfyUI did not return a prompt_id: $($queued | ConvertTo-Json -Depth 10)"
    }
    $promptId = $queued.prompt_id
    Write-Host "${Stage}_PROMPT_ID=$promptId"
    $started = Get-Date
    $lastProgress = -20
    while ($true) {
        Start-Sleep -Seconds 2
        $history = Invoke-RestMethod -Uri "$Server/history/$promptId" -Method Get -TimeoutSec 30
        $entry = $history.$promptId
        if ($entry) {
            if ($entry.status.status_str -eq 'error') {
                throw "ComfyUI $Stage failed: $($entry.status.messages | ConvertTo-Json -Depth 30)"
            }
            if ($entry.status.completed -and $entry.outputs) {
                $elapsed = ((Get-Date) - $started).TotalSeconds
                Write-Host ("${Stage}_ELAPSED_SECONDS=" + [math]::Round($elapsed, 2))
                return $entry
            }
        }
        $elapsed = ((Get-Date) - $started).TotalSeconds
        if ($elapsed -gt $TimeoutSeconds) {
            throw "Timed out after $TimeoutSeconds seconds waiting for $Stage."
        }
        if ($elapsed -ge ($lastProgress + 20)) {
            $lastProgress = [int]$elapsed
            Write-Host ("${Stage}_WAITING_SECONDS=" + [int]$elapsed)
        }
    }
}

function Clear-ComfyModels {
    $body = @{ unload_models = $true; free_memory = $true } | ConvertTo-Json -Compress
    Invoke-RestMethod -Uri "$Server/free" -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 30 | Out-Null
    Start-Sleep -Seconds 3
}

function Get-LatentName {
    param([Parameter(Mandatory = $true)]$Entry, [Parameter(Mandatory = $true)][string]$NodeId)
    $record = $Entry.outputs.$NodeId.h3_latents | Select-Object -First 1
    if (-not $record) {
        throw "Node $NodeId returned no h3_latents record."
    }
    if ($record.subfolder) {
        return "$($record.subfolder)/$($record.filename)"
    }
    return $record.filename
}

$conditioningInputs = @{
    clip = @('128', 0)
    vae = @('119', 0)
    audio_vae = @('120', 0)
    prompt = $promptText
    width = $width
    height = $height
    length = $frameLength
    ref_image_size = $refImageSize
}

$samplePrompt = @{
    '119' = @{ class_type = 'VAELoader'; inputs = @{ vae_name = 'minimax_h3_video_vae_fp16.safetensors' } }
    '120' = @{ class_type = 'VAELoader'; inputs = @{ vae_name = 'minimax_h3_audio_vae_fp32.safetensors' } }
    '123' = @{ class_type = 'KSamplerSelect'; inputs = @{ sampler_name = 'res_multistep' } }
    '124' = @{ class_type = 'BasicScheduler'; inputs = @{ model = @('127', 0); scheduler = 'simple'; steps = $steps; denoise = 1.0 } }
    '125' = @{ class_type = 'SamplerCustomAdvanced'; inputs = @{ noise = @('129', 0); guider = @('126', 0); sampler = @('123', 0); sigmas = @('124', 0); latent_image = @('136', 1) } }
    '126' = @{ class_type = 'BasicGuider'; inputs = @{ model = @('127', 0); conditioning = @('136', 0) } }
    '127' = @{ class_type = 'UNETLoader'; inputs = @{ unet_name = 'minimax_h3_ref2va_pruned_int8_convrot.safetensors'; weight_dtype = 'default' } }
    '128' = @{ class_type = 'CLIPLoader'; inputs = @{ clip_name = 'qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors'; type = 'minimax'; device = 'default' } }
    '129' = @{ class_type = 'RandomNoise'; inputs = @{ noise_seed = $seed } }
    '136' = @{ class_type = 'MiniMaxH3ReferenceToVideo'; inputs = $conditioningInputs }
    '200' = @{ class_type = 'SaveMiniMaxH3Latent'; inputs = @{ samples = @('125', 0); filename_prefix = $latentPrefix } }
}

$nextNode = 400
for ($index = 0; $index -lt $config.ref_images.Count; $index++) {
    $nodeId = [string]$nextNode
    $nextNode++
    $samplePrompt[$nodeId] = @{ class_type = 'LoadImage'; inputs = @{ image = [string]$config.ref_images[$index] } }
    $conditioningInputs["ref_images.ref_image_$index"] = @($nodeId, 0)
}
for ($index = 0; $index -lt $config.ref_videos.Count; $index++) {
    $loadId = [string]$nextNode
    $nextNode++
    $componentsId = [string]$nextNode
    $nextNode++
    $reference = $config.ref_videos[$index]
    $samplePrompt[$loadId] = @{ class_type = 'LoadVideo'; inputs = @{ file = [string]$reference.file } }
    $samplePrompt[$componentsId] = @{ class_type = 'GetVideoComponents'; inputs = @{ video = @($loadId, 0) } }
    $conditioningInputs["ref_videos.ref_video_$index"] = @($componentsId, 0)
    if ($reference.use_audio) {
        $conditioningInputs["ref_video_audios.ref_video_audio_$index"] = @($componentsId, 1)
    }
}
for ($index = 0; $index -lt $config.ref_audios.Count; $index++) {
    $nodeId = [string]$nextNode
    $nextNode++
    $samplePrompt[$nodeId] = @{ class_type = 'LoadAudio'; inputs = @{ audio = [string]$config.ref_audios[$index] } }
    $conditioningInputs["ref_audios.ref_audio_$index"] = @($nodeId, 0)
}

Write-Host "R2V_REFERENCES=images:$($config.ref_images.Count),videos:$($config.ref_videos.Count),audios:$($config.ref_audios.Count)"
Write-Host "R2V_OUTPUT=${width}x${height},frames:$frameLength,steps:$steps,ref_image_size:$refImageSize"

Clear-ComfyModels
$sampleResult = Invoke-ComfyPrompt -Prompt $samplePrompt -Stage 'R2V_SAMPLE'
$latentName = Get-LatentName -Entry $sampleResult -NodeId '200'
Write-Host "LATENT=$latentName"

Clear-ComfyModels
$decodePrompt = @{
    '10' = @{ class_type = 'VAEDecode'; inputs = @{ samples = @('300', 0); vae = @('11', 0) } }
    '11' = @{ class_type = 'VAELoader'; inputs = @{ vae_name = 'minimax_h3_video_vae_fp16.safetensors' } }
    '23' = @{ class_type = 'VAEDecodeAudio'; inputs = @{ samples = @('300', 0); vae = @('24', 0) } }
    '24' = @{ class_type = 'VAELoader'; inputs = @{ vae_name = 'minimax_h3_audio_vae_fp32.safetensors' } }
    '91' = @{ class_type = 'CreateVideo'; inputs = @{ images = @('10', 0); audio = @('23', 0); fps = 24.0; bit_depth = 8 } }
    '92' = @{ class_type = 'SaveVideo'; inputs = @{ video = @('91', 0); filename_prefix = $outputPrefix; format = 'auto'; codec = 'auto' } }
    '300' = @{ class_type = 'LoadMiniMaxH3Latent'; inputs = @{ filename = $latentName } }
}
$decodeResult = Invoke-ComfyPrompt -Prompt $decodePrompt -Stage 'DECODE'
Write-Host 'VIDEO_OUTPUT_BEGIN'
$decodeResult.outputs.'92' | ConvertTo-Json -Depth 30
Write-Host 'VIDEO_OUTPUT_END'
Clear-ComfyModels
