# ============================================================================
# Dream Server Windows CLI -- dream.ps1
# ============================================================================
# Day-to-day management of a Dream Server installation on Windows.
# Mirrors the Linux dream-cli command structure.
#
# Usage:
#   .\dream.ps1 status              # Health checks + GPU status
#   .\dream.ps1 start [service]     # Start all or one service
#   .\dream.ps1 stop [service]      # Stop all or one service
#   .\dream.ps1 restart [service]   # Restart all or one service
#   .\dream.ps1 logs <service> [N]  # Tail logs (default 100 lines)
#   .\dream.ps1 config show         # View .env (secrets masked)
#   .\dream.ps1 config edit         # Open .env in notepad
#   .\dream.ps1 chat "message"      # Quick chat via API
#   .\dream.ps1 update              # Pull latest images and restart
#   .\dream.ps1 report              # Generate Windows diagnostics bundle
#   .\dream.ps1 version             # Show version
#   .\dream.ps1 help                # Show help
#
# ============================================================================

[CmdletBinding()]
param(
    [Parameter(Position = 0)]
    [string]$Command = "help",

    [Parameter(Position = 1, ValueFromRemainingArguments = $true)]
    [string[]]$Arguments
)

$ErrorActionPreference = "Stop"

# ── Locate libraries ──
# NOTE: Nested Join-Path required -- PS 5.1 only accepts 2 arguments
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$LibDir = Join-Path $ScriptDir "lib"
. (Join-Path $LibDir "constants.ps1")
. (Join-Path $LibDir "ui.ps1")
. (Join-Path $LibDir "compose-diagnostics.ps1")
. (Join-Path $LibDir "detection.ps1")
. (Join-Path $LibDir "llm-endpoint.ps1")
. (Join-Path $LibDir "install-report.ps1")

# ── Resolve install directory ──
$InstallDir = $script:DS_INSTALL_DIR

# ============================================================================
# Helpers
# ============================================================================

function Test-DockerRunning {
    <#
    .SYNOPSIS
        Quick check if Docker daemon is responsive. Shows friendly message if not.
    #>
    $null = docker info 2>$null
    if ($LASTEXITCODE -ne 0) {
        Write-AIError "Docker Desktop is not running."
        Write-AI "Start it from the Start Menu, then try again."
        return $false
    }
    return $true
}

function Test-Install {
    if (-not (Test-Path $InstallDir)) {
        Write-AIError "Dream Server not found at $InstallDir. Set DREAM_HOME or run installer first."
        exit 1
    }
    $baseCompose = Join-Path $InstallDir "docker-compose.base.yml"
    $monoCompose = Join-Path $InstallDir "docker-compose.yml"
    if (-not (Test-Path $baseCompose) -and -not (Test-Path $monoCompose)) {
        Write-AIError "docker-compose.base.yml not found in $InstallDir"
        exit 1
    }
    if (-not (Test-DockerRunning)) { exit 1 }
}

function Get-ComposeFlags {
    <#
    .SYNOPSIS
        Read saved compose flags from installer, or build default flags.
    #>
    $flagsFile = Join-Path $InstallDir ".compose-flags"
    if (Test-Path $flagsFile) {
        $raw = (Get-Content $flagsFile -Raw).Trim()
        return ($raw -split "\s+")
    }

    # Fallback: detect from available files
    # --env-file explicit: Docker Compose V2 on Windows may not auto-discover
    # .env from the project directory when multiple -f flags are used.
    $flags = @("--env-file", ".env")
    $base = Join-Path $InstallDir "docker-compose.base.yml"
    $nvidia = Join-Path $InstallDir "docker-compose.nvidia.yml"
    $mono = Join-Path $InstallDir "docker-compose.yml"

    if (Test-Path $base) {
        $flags += @("-f", "docker-compose.base.yml")
        if (Test-Path $nvidia) {
            $flags += @("-f", "docker-compose.nvidia.yml")
        }
    } elseif (Test-Path $mono) {
        $flags += @("-f", "docker-compose.yml")
    }

    # Add enabled extension compose files
    $extDir = Join-Path (Join-Path $InstallDir "extensions") "services"
    if (Test-Path $extDir) {
        Get-ChildItem -Path $extDir -Directory | ForEach-Object {
            $composePath = Join-Path $_.FullName "compose.yaml"
            if (Test-Path $composePath) {
                $relPath = $composePath.Substring($InstallDir.Length + 1) -replace "\\", "/"
                $flags += @("-f", $relPath)
            }
        }
    }

    return $flags
}

function Read-DreamEnv {
    <#
    .SYNOPSIS
        Safely load .env file into a hashtable (no eval, no injection).
    #>
    return Get-WindowsDreamEnvMap -InstallDir $InstallDir
}

function Set-DreamEnvValue {
    <#
    .SYNOPSIS
        Upsert a KEY=VALUE pair in .env without adding a UTF-8 BOM.
    #>
    param(
        [string]$Key,
        [string]$Value
    )

    $envFile = Join-Path $InstallDir ".env"
    if (-not (Test-Path $envFile)) { return }

    $lines = New-Object 'System.Collections.Generic.List[string]'
    Get-Content $envFile | ForEach-Object { [void]$lines.Add($_) }

    $escapedKey = [regex]::Escape($Key)
    $updated = $false
    for ($i = 0; $i -lt $lines.Count; $i++) {
        if ($lines[$i] -match "^${escapedKey}=") {
            $lines[$i] = "${Key}=${Value}"
            $updated = $true
            break
        }
    }

    if (-not $updated) {
        [void]$lines.Add("${Key}=${Value}")
    }

    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
    [System.IO.File]::WriteAllLines($envFile, $lines.ToArray(), $utf8NoBom)
}

function Select-AutoCpuValue {
    <#
    .SYNOPSIS
        Keep a manual CPU override only when it is valid and more conservative.
    #>
    param(
        [string]$Existing,
        [string]$Detected
    )

    $existingNumber = 0.0
    $detectedNumber = 0.0
    $style = [System.Globalization.NumberStyles]::Float
    $culture = [System.Globalization.CultureInfo]::InvariantCulture
    $existingValid = [double]::TryParse($Existing, $style, $culture, [ref]$existingNumber)
    $detectedValid = [double]::TryParse($Detected, $style, $culture, [ref]$detectedNumber)

    if ($existingValid -and $detectedValid -and $existingNumber -gt 0 -and $existingNumber -le $detectedNumber) {
        return $Existing
    }
    return $Detected
}

function Ensure-LlamaCpuBudget {
    <#
    .SYNOPSIS
        Backfill/cap llama-server CPU settings for existing installs.
    #>
    $envFile = Join-Path $InstallDir ".env"
    if (-not (Test-Path $envFile)) { return }

    $envVars = Read-DreamEnv
    $gpuBackend = $envVars["GPU_BACKEND"]
    if ([string]::IsNullOrWhiteSpace($gpuBackend) -or $gpuBackend -eq "none") {
        $gpuBackend = "cpu"
    }
    $gpuBackend = $gpuBackend.ToLowerInvariant()

    $budget = Get-LlamaCpuBudget -GpuBackend $gpuBackend
    $llamaCpuLimit = Select-AutoCpuValue -Existing $envVars["LLAMA_CPU_LIMIT"] -Detected $budget.Limit
    $llamaCpuReservation = Select-AutoCpuValue -Existing $envVars["LLAMA_CPU_RESERVATION"] -Detected $budget.Reservation

    $limitNumber = 0.0
    $reservationNumber = 0.0
    $style = [System.Globalization.NumberStyles]::Float
    $culture = [System.Globalization.CultureInfo]::InvariantCulture
    if ([double]::TryParse($llamaCpuLimit, $style, $culture, [ref]$limitNumber) -and
        [double]::TryParse($llamaCpuReservation, $style, $culture, [ref]$reservationNumber) -and
        $reservationNumber -gt $limitNumber) {
        $llamaCpuReservation = $llamaCpuLimit
    }

    $changed = $false
    if ($envVars["LLAMA_CPU_LIMIT"] -ne $llamaCpuLimit) {
        Set-DreamEnvValue -Key "LLAMA_CPU_LIMIT" -Value $llamaCpuLimit
        $changed = $true
    }
    if ($envVars["LLAMA_CPU_RESERVATION"] -ne $llamaCpuReservation) {
        Set-DreamEnvValue -Key "LLAMA_CPU_RESERVATION" -Value $llamaCpuReservation
        $changed = $true
    }

    if ($changed) {
        Write-AI ("Auto-adjusted llama-server CPU budget: limit={0}, reservation={1} (Docker CPUs: {2})" -f `
            $llamaCpuLimit, $llamaCpuReservation, $budget.Available)
    }
}

# ── AMD native inference server management (Lemonade or llama-server) ──

function Get-NativeInferenceBackend {
    <#
    .SYNOPSIS
        Determine which native inference backend is configured (from .env LLM_BACKEND).
    #>
    $env = Read-DreamEnv
    $backend = $env["LLM_BACKEND"]
    if ($backend -eq "lemonade" -and (Test-Path $script:LEMONADE_EXE)) { return "lemonade" }
    if (Test-Path $script:LLAMA_SERVER_EXE) { return "llama-server" }
    return "none"
}

function Get-NativeInferenceStatus {
    <#
    .SYNOPSIS
        Check if native inference server is running (AMD path: Lemonade or llama-server).
    .OUTPUTS
        @{ Running; Pid; Healthy; Backend }
    #>
    $backend = Get-NativeInferenceBackend
    $result = @{ Running = $false; Pid = 0; Healthy = $false; Backend = $backend }

    if (-not (Test-Path $script:INFERENCE_PID_FILE)) { return $result }

    $savedPid = [int](Get-Content $script:INFERENCE_PID_FILE -Raw).Trim()
    try {
        $proc = Get-Process -Id $savedPid -ErrorAction SilentlyContinue
        if ($proc -and -not $proc.HasExited) {
            $result.Running = $true
            $result.Pid = $savedPid

            # Health check (Lemonade uses /api/v1/health, llama-server uses /health)
            $healthUrl = $(if ($backend -eq "lemonade") { $script:LEMONADE_HEALTH_URL } else { "http://localhost:8080/health" })
            try {
                $resp = Invoke-WebRequest -Uri $healthUrl `
                    -TimeoutSec 3 -UseBasicParsing -ErrorAction SilentlyContinue
                if ($resp.StatusCode -eq 200) {
                    $result.Healthy = $true
                }
            } catch { }
        }
    } catch { }

    # Clean up stale PID file
    if (-not $result.Running -and (Test-Path $script:INFERENCE_PID_FILE)) {
        Remove-Item $script:INFERENCE_PID_FILE -Force -ErrorAction SilentlyContinue
    }

    return $result
}

# Backward-compat alias
function Get-NativeLlamaStatus { return Get-NativeInferenceStatus }

function Start-NativeInferenceServer {
    <#
    .SYNOPSIS
        Start native inference server for AMD path (Lemonade or llama-server).
    #>
    $status = Get-NativeInferenceStatus
    if ($status.Running) {
        Write-AISuccess "Native $($status.Backend) already running (PID $($status.Pid))"
        return
    }

    $backend = Get-NativeInferenceBackend
    $envVars = Read-DreamEnv

    # Honour the unified BIND_ADDRESS knob (PR #964); empty/missing → loopback.
    $bindAddr = $envVars["BIND_ADDRESS"]
    if ([string]::IsNullOrWhiteSpace($bindAddr)) { $bindAddr = "127.0.0.1" }

    if ($backend -eq "lemonade") {
        $modelsDir = Join-Path (Join-Path $InstallDir "data") "models"
        $lemonadeArgs = @(
            "serve",
            "--port", "$($script:LEMONADE_PORT)",
            "--host", $bindAddr,
            "--no-tray",
            "--llamacpp", "vulkan",
            "--extra-models-dir", $modelsDir
        )
        $pidDir = Split-Path $script:INFERENCE_PID_FILE
        New-Item -ItemType Directory -Path $pidDir -Force | Out-Null

        $proc = Start-Process -FilePath $script:LEMONADE_EXE `
            -ArgumentList $lemonadeArgs -WindowStyle Hidden -PassThru
        Set-Content -Path $script:INFERENCE_PID_FILE -Value $proc.Id

        Write-AISuccess "Lemonade server started (PID $($proc.Id))"
        Write-AI "Waiting for health..."

        $maxWait = 60; $waited = 0
        while ($waited -lt $maxWait) {
            Start-Sleep -Seconds 2; $waited += 2
            try {
                $resp = Invoke-WebRequest -Uri $script:LEMONADE_HEALTH_URL `
                    -TimeoutSec 3 -UseBasicParsing -ErrorAction SilentlyContinue
                if ($resp.StatusCode -eq 200) {
                    Write-AISuccess "Lemonade server healthy"
                    return
                }
            } catch { }
        }
        Write-AIWarn "Lemonade server may still be starting..."
    } elseif ($backend -eq "llama-server") {
        $ggufFile = $envVars["GGUF_FILE"]
        $ctxSize  = $envVars["CTX_SIZE"]
        if (-not $ggufFile) { $ggufFile = "Qwen3.5-9B-Q4_K_M.gguf" }
        if (-not $ctxSize)  { $ctxSize = "16384" }

        $modelPath = Join-Path (Join-Path $InstallDir "data\models") $ggufFile
        if (-not (Test-Path $modelPath)) {
            Write-AIError "Model not found: $modelPath"
            return
        }

        $llamaArgs = @(
            "--model", $modelPath,
            "--host", $bindAddr,
            "--port", "8080",
            "--n-gpu-layers", "999",
            "--ctx-size", $ctxSize
        )
        if ($envVars["LLAMA_ARG_FLASH_ATTN"]) { $llamaArgs += @("--flash-attn", $envVars["LLAMA_ARG_FLASH_ATTN"]) }
        if ($envVars["LLAMA_ARG_CACHE_TYPE_K"]) { $llamaArgs += @("--cache-type-k", $envVars["LLAMA_ARG_CACHE_TYPE_K"]) }
        if ($envVars["LLAMA_ARG_CACHE_TYPE_V"]) { $llamaArgs += @("--cache-type-v", $envVars["LLAMA_ARG_CACHE_TYPE_V"]) }
        if ($envVars["LLAMA_ARG_N_CPU_MOE"]) { $llamaArgs += @("--n-cpu-moe", $envVars["LLAMA_ARG_N_CPU_MOE"]) }

        $pidDir = Split-Path $script:INFERENCE_PID_FILE
        New-Item -ItemType Directory -Path $pidDir -Force | Out-Null

        $proc = Start-Process -FilePath $script:LLAMA_SERVER_EXE `
            -ArgumentList $llamaArgs -WindowStyle Hidden -PassThru
        Set-Content -Path $script:INFERENCE_PID_FILE -Value $proc.Id

        Write-AISuccess "Native llama-server started (PID $($proc.Id))"
        Write-AI "Waiting for health..."

        $maxWait = 60; $waited = 0
        while ($waited -lt $maxWait) {
            Start-Sleep -Seconds 2; $waited += 2
            try {
                $resp = Invoke-WebRequest -Uri "http://localhost:8080/health" `
                    -TimeoutSec 3 -UseBasicParsing -ErrorAction SilentlyContinue
                if ($resp.StatusCode -eq 200) {
                    Write-AISuccess "Native llama-server healthy"
                    return
                }
            } catch { }
        }
        Write-AIWarn "llama-server may still be loading model..."
    } else {
        Write-AIError "No native inference server found. Re-run the installer."
    }
}

# Backward-compat alias
function Start-NativeLlamaServer { Start-NativeInferenceServer }

function Stop-NativeInferenceServer {
    $status = Get-NativeInferenceStatus
    if (-not $status.Running) {
        Write-AI "Native inference server not running"
        return
    }

    try {
        Stop-Process -Id $status.Pid -Force -ErrorAction SilentlyContinue
        Write-AISuccess "Native $($status.Backend) stopped (PID $($status.Pid))"
    } catch {
        Write-AIWarn "Could not stop PID $($status.Pid): $_"
    }

    if (Test-Path $script:INFERENCE_PID_FILE) {
        Remove-Item $script:INFERENCE_PID_FILE -Force -ErrorAction SilentlyContinue
    }
}

# Backward-compat alias
function Stop-NativeLlamaServer { Stop-NativeInferenceServer }

# ============================================================================
# Commands
# ============================================================================

function Invoke-Status {
    Test-Install
    Push-Location $InstallDir
    try {
        $flags = Get-ComposeFlags
        Write-Host ""
        Write-Host "  Dream Server Status" -ForegroundColor Cyan
        Write-Host ("  " + ("-" * 40)) -ForegroundColor DarkGray

        # Native inference server status (AMD: Lemonade or llama-server)
        if (Test-Path $script:INFERENCE_PID_FILE) {
            $nativeStatus = Get-NativeInferenceStatus
            if ($nativeStatus.Running) {
                $healthStr = $(if ($nativeStatus.Healthy) { "healthy" } else { "loading" })
                Write-AISuccess "$($nativeStatus.Backend) (native): running PID $($nativeStatus.Pid) ($healthStr)"
            } else {
                Write-AIWarn "$($nativeStatus.Backend) (native): not running (stale PID cleaned)"
            }
        }

        # Host agent status
        try {
            $resp = Invoke-WebRequest -Uri $script:DREAM_AGENT_HEALTH_URL `
                -TimeoutSec 3 -UseBasicParsing -ErrorAction SilentlyContinue
            if ($resp.StatusCode -eq 200) {
                Write-AISuccess "Host Agent: running (port $($script:DREAM_AGENT_PORT))"
            } else {
                Write-AIWarn "Host Agent: responded with $($resp.StatusCode)"
            }
        } catch {
            Write-AIWarn "Host Agent: not responding (port $($script:DREAM_AGENT_PORT))"
        }

        # Docker services
        Write-Host ""
        & docker compose @flags ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" 2>$null

        # Health checks
        Write-Host ""
        Write-Host "  Health Checks" -ForegroundColor Cyan
        Write-Host ("  " + ("-" * 40)) -ForegroundColor DarkGray

        $llmEndpoint = Get-WindowsLocalLlmEndpoint -InstallDir $InstallDir -NativeBackend (Get-NativeInferenceBackend)
        $endpoints = @(
            @{ Name = "LLM API";    Url = $llmEndpoint.HealthUrl }
            @{ Name = "Chat UI";    Url = "http://localhost:3000" }
            @{ Name = "Dashboard";  Url = "http://localhost:3001" }
        )

        foreach ($ep in $endpoints) {
            try {
                $resp = Invoke-WebRequest -Uri $ep.Url -TimeoutSec 3 `
                    -UseBasicParsing -ErrorAction SilentlyContinue
                if ($resp.StatusCode -ge 200 -and $resp.StatusCode -lt 400) {
                    Write-AISuccess "$($ep.Name): healthy"
                } else {
                    Write-AIWarn "$($ep.Name): $($resp.StatusCode)"
                }
            } catch {
                Write-AIWarn "$($ep.Name): not responding"
            }
        }

        # GPU status
        Write-Host ""
        $gpuInfo = Get-GpuInfo
        if ($gpuInfo.Backend -eq "nvidia") {
            Write-Host "  GPU Status" -ForegroundColor Cyan
            try {
                $gpuStats = & nvidia-smi --query-gpu=name,utilization.gpu,memory.used,memory.total,temperature.gpu --format=csv,noheader,nounits 2>$null
                if ($gpuStats) {
                    $gpuStats -split "`n" | ForEach-Object {
                        $parts = $_ -split ","
                        if ($parts.Count -ge 5) {
                            Write-Host "  $($parts[0].Trim()): $($parts[1].Trim())% GPU | $($parts[2].Trim())MB/$($parts[3].Trim())MB VRAM | $($parts[4].Trim())C" -ForegroundColor White
                        }
                    }
                }
            } catch { }
        } elseif ($gpuInfo.Backend -eq "amd") {
            Write-Host "  GPU: $($gpuInfo.Name) ($($gpuInfo.MemoryType) memory)" -ForegroundColor White
        }

        Write-Host ""
    } finally {
        Pop-Location
    }
}

function Invoke-Start {
    param([string]$Service)
    Test-Install
    Push-Location $InstallDir
    try {
        Ensure-LlamaCpuBudget

        # Start native inference server first (AMD path: Lemonade or llama-server)
        if (-not $Service -and ((Get-NativeInferenceBackend) -ne "none")) {
            Start-NativeInferenceServer
        }

        # Start host agent (if not already running)
        if (-not $Service) {
            Invoke-Agent -Action "start"
        }

        $flags = Get-ComposeFlags
        if ($Service) {
            Write-AI "Starting $Service..."
            $composeExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags `
                -ComposeArgs @("up", "-d", $Service)
            if ($composeExit -ne 0) {
                Write-AIError "docker compose up failed (exit code: $composeExit)"
                Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags `
                    -Phase "dream.ps1 start ($Service)"
                exit 1
            }
            Write-AISuccess "$Service started"
        } else {
            Write-AI "Starting all services..."
            $composeExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags `
                -ComposeArgs @("up", "-d")
            if ($composeExit -ne 0) {
                Write-AIError "docker compose up failed (exit code: $composeExit)"
                Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags -Phase "dream.ps1 start (all)"
                exit 1
            }
            Write-AISuccess "All services started"
        }
    } finally {
        Pop-Location
    }
}

function Invoke-Stop {
    param([string]$Service)
    Test-Install
    Push-Location $InstallDir
    try {
        $flags = Get-ComposeFlags
        if ($Service) {
            Write-AI "Stopping $Service..."
            $composeExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags `
                -ComposeArgs @("stop", $Service)
            if ($composeExit -ne 0) {
                Write-AIError "docker compose stop failed (exit code: $composeExit)"
                Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags `
                    -Phase "dream.ps1 stop ($Service)"
                exit 1
            }
            Write-AISuccess "$Service stopped"
        } else {
            Write-AI "Stopping all services..."
            $composeExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags `
                -ComposeArgs @("down")
            if ($composeExit -ne 0) {
                Write-AIError "docker compose down failed (exit code: $composeExit)"
                Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags -Phase "dream.ps1 stop (all)"
                exit 1
            }

            # Stop native inference server (AMD path)
            if (Test-Path $script:INFERENCE_PID_FILE) {
                Stop-NativeInferenceServer
            }

            # Stop host agent
            Invoke-Agent -Action "stop"

            Write-AISuccess "All services stopped"
        }
    } finally {
        Pop-Location
    }
}

function Invoke-Restart {
    param([string]$Service)
    Test-Install
    Push-Location $InstallDir
    try {
        Ensure-LlamaCpuBudget

        $flags = Get-ComposeFlags
        if ($Service) {
            Write-AI "Restarting $Service..."
            $composeExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags `
                -ComposeArgs @("restart", $Service)
            if ($composeExit -ne 0) {
                Write-AIError "docker compose restart failed (exit code: $composeExit)"
                Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags `
                    -Phase "dream.ps1 restart ($Service)"
                exit 1
            }
            Write-AISuccess "$Service restarted"
        } else {
            # For AMD, also restart native inference server
            if (Test-Path $script:INFERENCE_PID_FILE) {
                Stop-NativeInferenceServer
                Start-NativeInferenceServer
            }
            Write-AI "Restarting all services..."
            $composeExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags `
                -ComposeArgs @("restart")
            if ($composeExit -ne 0) {
                Write-AIError "docker compose restart failed (exit code: $composeExit)"
                Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags -Phase "dream.ps1 restart (all)"
                exit 1
            }
            Write-AISuccess "All services restarted"
        }
    } finally {
        Pop-Location
    }
}

function Invoke-Logs {
    param(
        [string]$Service,
        [int]$Lines = 100
    )
    if (-not $Service) {
        Write-AI "Usage: .\dream.ps1 logs <service> [lines]"
        Write-AI "Services: llama-server, open-webui, dashboard-api, n8n, whisper, tts, ..."
        return
    }
    Test-Install
    Push-Location $InstallDir
    try {
        $flags = Get-ComposeFlags
        & docker compose @flags logs -f --tail $Lines $Service
    } finally {
        Pop-Location
    }
}

function Invoke-ConfigShow {
    Test-Install
    Write-Host ""
    Write-Host "  Configuration" -ForegroundColor Cyan
    Write-Host "  Install dir: $InstallDir" -ForegroundColor White
    Write-Host ""

    $envFile = Join-Path $InstallDir ".env"
    if (-not (Test-Path $envFile)) {
        Write-AIWarn ".env not found"
        return
    }

    Get-Content $envFile | ForEach-Object {
        $line = $_.Trim()
        if ($line -match "^#" -or $line -eq "") { return }
        if ($line -match "(SECRET|PASS|TOKEN|KEY)=") {
            $key = ($line -split "=")[0]
            Write-Host "  $key=***" -ForegroundColor DarkGray
        } else {
            Write-Host "  $line" -ForegroundColor White
        }
    }
    Write-Host ""
}

function Invoke-Chat {
    param([string]$Message)
    if (-not $Message) {
        Write-AI "Usage: .\dream.ps1 chat `"your message`""
        return
    }

    $body = @{
        model    = "default"
        messages = @(
            @{ role = "user"; content = $Message }
        )
    } | ConvertTo-Json -Depth 3

    $llmEndpoint = Get-WindowsLocalLlmEndpoint -InstallDir $InstallDir -NativeBackend (Get-NativeInferenceBackend)
    try {
        $resp = Invoke-RestMethod -Uri $llmEndpoint.ChatCompletionsUrl `
            -Method POST -Body $body -ContentType "application/json" -TimeoutSec 120

        if ($resp.choices -and $resp.choices[0].message) {
            Write-Host ""
            Write-Host $resp.choices[0].message.content
            Write-Host ""
        }
    } catch {
        Write-AIError "Chat request failed: $_"
        Write-AI "Is llama-server running? Try: .\dream.ps1 status"
    }
}

function Invoke-Update {
    Test-Install
    Push-Location $InstallDir
    try {
        Ensure-LlamaCpuBudget

        $flags = Get-ComposeFlags
        Write-AI "Pulling latest images..."
        $pullExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags -ComposeArgs @("pull")
        if ($pullExit -ne 0) {
            Write-AIError "docker compose pull failed (exit code: $pullExit)"
            Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags -Phase "dream.ps1 update (pull)"
            exit 1
        }
        Write-AI "Recreating containers..."
        $upExit = Invoke-DreamDockerCompose -InstallDir $InstallDir -ComposeFlags $flags `
            -ComposeArgs @("up", "-d", "--force-recreate")
        if ($upExit -ne 0) {
            Write-AIError "docker compose up failed (exit code: $upExit)"
            Write-DreamComposeDiagnostics -InstallDir $InstallDir -ComposeFlags $flags -Phase "dream.ps1 update (up --force-recreate)"
            exit 1
        }
        Write-AISuccess "Update complete"

        Start-Sleep -Seconds 5
        Invoke-Status
    } finally {
        Pop-Location
    }
}

function Invoke-Report {
    Test-Install
    Push-Location $InstallDir
    try {
        $flags = Get-ComposeFlags
        Write-DreamInstallReport -InstallDir $InstallDir -ComposeFlags $flags | Out-Null
    } finally {
        Pop-Location
    }
}

function Invoke-Agent {
    param([string]$Action = "status")

    $agentScript = Join-Path (Join-Path $InstallDir "bin") "dream-host-agent.py"
    $pidFile     = $script:DREAM_AGENT_PID_FILE
    $logFile     = $script:DREAM_AGENT_LOG_FILE
    $port        = $script:DREAM_AGENT_PORT
    $healthUrl   = $script:DREAM_AGENT_HEALTH_URL

    switch ($Action.ToLower()) {
        "status" {
            try {
                $resp = Invoke-WebRequest -Uri $healthUrl -TimeoutSec 3 `
                    -UseBasicParsing -ErrorAction SilentlyContinue
                if ($resp.StatusCode -eq 200) {
                    Write-AISuccess "Host agent: running (port $port)"
                } else {
                    Write-AIWarn "Host agent: responded with status $($resp.StatusCode)"
                }
            } catch {
                Write-AIWarn "Host agent: not responding (port $port)"
            }
        }
        "start" {
            # Check if already running
            try {
                $resp = Invoke-WebRequest -Uri $healthUrl -TimeoutSec 2 `
                    -UseBasicParsing -ErrorAction SilentlyContinue
                if ($resp.StatusCode -eq 200) {
                    Write-AISuccess "Host agent already running (port $port)"
                    return
                }
            } catch { }

            # Find Python
            $_python3 = Get-Command python3 -ErrorAction SilentlyContinue
            if (-not $_python3) { $_python3 = Get-Command python -ErrorAction SilentlyContinue }
            if (-not $_python3) {
                Write-AIError "Python not found in PATH -- install Python 3 and try again"
                return
            }
            if (-not (Test-Path $agentScript)) {
                Write-AIError "Agent script not found: $agentScript"
                return
            }

            # Clean stale PID
            if (Test-Path $pidFile) {
                try {
                    $_oldPid = [int](Get-Content $pidFile -Raw).Trim()
                    Stop-Process -Id $_oldPid -Force -ErrorAction SilentlyContinue
                } catch { }
                Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
            }

            $pidDir = Split-Path $pidFile
            New-Item -ItemType Directory -Path $pidDir -Force -ErrorAction SilentlyContinue | Out-Null

            # Start agent through a hidden PowerShell host so manual starts do
            # not leave a visible cmd.exe window. Prepend Docker to PATH so the
            # agent can find docker.exe (Docker Desktop may not be in the
            # system PATH yet after fresh install).
            $_dockerBin = "C:\Program Files\Docker\Docker\resources\bin"
            $_psQuote = {
                param([string]$Value)
                "'" + ($Value -replace "'", "''") + "'"
            }
            $_dockerPathLiteral = & $_psQuote "$_dockerBin;"
            $_pythonLiteral = & $_psQuote $_python3.Source
            $_agentScriptLiteral = & $_psQuote $agentScript
            $_pidFileLiteral = & $_psQuote $pidFile
            $_installDirLiteral = & $_psQuote $InstallDir
            $_logFileLiteral = & $_psQuote $logFile
            $_agentCommand = @"
`$env:PATH = $_dockerPathLiteral + `$env:PATH
`$agentArgs = @($_agentScriptLiteral, '--port', '$port', '--pid-file', $_pidFileLiteral, '--install-dir', $_installDirLiteral)
Start-Process -FilePath $_pythonLiteral -ArgumentList `$agentArgs -WorkingDirectory $_installDirLiteral -WindowStyle Hidden -RedirectStandardError $_logFileLiteral
"@
            $_encodedAgentCommand = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($_agentCommand))
            Start-Process -FilePath "powershell.exe" `
                -ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-EncodedCommand", $_encodedAgentCommand `
                -WindowStyle Hidden -WorkingDirectory $InstallDir

            Start-Sleep -Seconds 3
            try {
                $resp = Invoke-WebRequest -Uri $healthUrl -TimeoutSec 3 `
                    -UseBasicParsing -ErrorAction SilentlyContinue
                if ($resp.StatusCode -eq 200) {
                    Write-AISuccess "Host agent started (port $port)"
                } else {
                    Write-AIWarn "Host agent started but health check returned $($resp.StatusCode)"
                }
            } catch {
                Write-AIWarn "Host agent started but not yet responding -- check: .\dream.ps1 agent status"
            }
        }
        "stop" {
            if (Test-Path $pidFile) {
                try {
                    $_pid = [int](Get-Content $pidFile -Raw).Trim()
                    Stop-Process -Id $_pid -Force -ErrorAction SilentlyContinue
                    Write-AISuccess "Host agent stopped (PID $_pid)"
                } catch {
                    Write-AIWarn "Could not stop agent PID: $_"
                }
                Remove-Item $pidFile -Force -ErrorAction SilentlyContinue
            } else {
                Write-AI "Host agent not running (no PID file)"
            }
        }
        "restart" {
            Invoke-Agent -Action "stop"
            Start-Sleep -Seconds 1
            Invoke-Agent -Action "start"
        }
        "logs" {
            if (Test-Path $logFile) {
                Get-Content $logFile -Tail 100 -Wait
            } else {
                Write-AIWarn "No log file at $logFile"
            }
        }
        default {
            Write-Host ""
            Write-Host "  Usage: .\dream.ps1 agent [status|start|stop|restart|logs]" -ForegroundColor DarkGray
            Write-Host ""
        }
    }
}

function Show-Help {
    Write-Host ""
    Write-Host "  Dream Server CLI (Windows)" -ForegroundColor Green
    Write-Host "  Version $($script:DS_VERSION)" -ForegroundColor DarkGray
    Write-Host ""
    Write-Host "  USAGE" -ForegroundColor White
    Write-Host "    .\dream.ps1 <command> [options]" -ForegroundColor DarkGray
    Write-Host ""
    Write-Host "  COMMANDS" -ForegroundColor White
    Write-Host "    status              " -ForegroundColor Cyan -NoNewline
    Write-Host "Health checks + GPU status" -ForegroundColor DarkGray
    Write-Host "    start [service]     " -ForegroundColor Cyan -NoNewline
    Write-Host "Start all or one service" -ForegroundColor DarkGray
    Write-Host "    stop [service]      " -ForegroundColor Cyan -NoNewline
    Write-Host "Stop all or one service" -ForegroundColor DarkGray
    Write-Host "    restart [service]   " -ForegroundColor Cyan -NoNewline
    Write-Host "Restart all or one service" -ForegroundColor DarkGray
    Write-Host "    logs <svc> [lines]  " -ForegroundColor Cyan -NoNewline
    Write-Host "Tail logs (default 100)" -ForegroundColor DarkGray
    Write-Host "    config show         " -ForegroundColor Cyan -NoNewline
    Write-Host "View .env (secrets masked)" -ForegroundColor DarkGray
    Write-Host "    config edit         " -ForegroundColor Cyan -NoNewline
    Write-Host "Open .env in notepad" -ForegroundColor DarkGray
    Write-Host "    chat `"message`"      " -ForegroundColor Cyan -NoNewline
    Write-Host "Quick chat via API" -ForegroundColor DarkGray
    Write-Host "    update              " -ForegroundColor Cyan -NoNewline
    Write-Host "Pull latest images and restart" -ForegroundColor DarkGray
    Write-Host "    agent [action]      " -ForegroundColor Cyan -NoNewline
    Write-Host "Host agent: status|start|stop|restart|logs" -ForegroundColor DarkGray
    Write-Host "    report              " -ForegroundColor Cyan -NoNewline
    Write-Host "Generate Windows diagnostics bundle" -ForegroundColor DarkGray
    Write-Host "    version             " -ForegroundColor Cyan -NoNewline
    Write-Host "Show version" -ForegroundColor DarkGray
    Write-Host "    help                " -ForegroundColor Cyan -NoNewline
    Write-Host "Show this help" -ForegroundColor DarkGray
    Write-Host ""
    Write-Host "  EXAMPLES" -ForegroundColor White
    Write-Host "    .\dream.ps1 status" -ForegroundColor DarkGray
    Write-Host "    .\dream.ps1 logs llama-server 50" -ForegroundColor DarkGray
    Write-Host "    .\dream.ps1 restart open-webui" -ForegroundColor DarkGray
    Write-Host "    .\dream.ps1 chat `"What is quantum computing?`"" -ForegroundColor DarkGray
    Write-Host ""
}

# ============================================================================
# Command Dispatch
# ============================================================================

switch ($Command.ToLower()) {
    "status"  { Invoke-Status }
    "start"   { Invoke-Start -Service ($Arguments | Select-Object -First 1) }
    "stop"    { Invoke-Stop -Service ($Arguments | Select-Object -First 1) }
    "restart" { Invoke-Restart -Service ($Arguments | Select-Object -First 1) }
    "logs"    {
        $svc = $Arguments | Select-Object -First 1
        $n = $(if ($Arguments.Count -ge 2) { [int]$Arguments[1] } else { 100 })
        Invoke-Logs -Service $svc -Lines $n
    }
    "config"  {
        $action = ($Arguments | Select-Object -First 1)
        if ($action -eq "edit") {
            Test-Install
            & notepad (Join-Path $InstallDir ".env")
        } else {
            Invoke-ConfigShow
        }
    }
    "chat"    { Invoke-Chat -Message ($Arguments -join " ") }
    "update"  { Invoke-Update }
    "report"  { Invoke-Report }
    "agent"   {
        $action = ($Arguments | Select-Object -First 1)
        if (-not $action) { $action = "status" }
        Invoke-Agent -Action $action
    }
    "version" { Write-Host "Dream Server v$($script:DS_VERSION) (Windows)" -ForegroundColor Green }
    "help"    { Show-Help }
    default   {
        Write-AIWarn "Unknown command: $Command"
        Show-Help
    }
}
