# Copyright (c) 2026 Progralink. Licensed under the DevCage License — see LICENSE.
#
# Resources.ps1 — Engine detection, resource limits, and host environment setup
# for the DevCage PowerShell launcher. Dot-sourced by devcage.ps1.
# Depends on: $ContainerEngine, $SandboxTmpDir, $ComposeFile, $EmbedDocker.

# ── Host environment setup ─────────────────────────────────────────
# Sets HOST_UID, TERM, cleans stale temp dirs, sets COMPOSE_IGNORE_ORPHANS.
function Setup-HostEnv {
    # Docker Desktop for Windows handles UID mapping transparently.
    # Default to 1000; override via HOST_UID env var for WSL2 or custom setups.
    if (-not $env:HOST_UID) {
        $env:HOST_UID = "1000"
    }

    # Terminal
    if (-not $env:TERM) { $env:TERM = "xterm-256color" }

    # Clean up stale resources from previous runs (e.g. killed)
    Get-ChildItem -Path ([System.IO.Path]::GetTempPath()) -Directory -Filter "devcage-*" -ErrorAction SilentlyContinue |
        Where-Object { $_.LastWriteTime -lt (Get-Date).AddMinutes(-30) -and -not $_.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint) } |
        Remove-Item -Recurse -Force -ErrorAction SilentlyContinue

    # Skip orphan container checks — saves 1-3s on WSL2
    $env:COMPOSE_IGNORE_ORPHANS = "1"
}

# ── DinD crash cleanup ────────────────────────────────────────────
# If a DinD sidecar survived a previous crash, clean its inner
# containers and tear it down before starting fresh.
function Invoke-ComposeCleanup {
    if ($EmbedDocker) {
        $prevEAP = $ErrorActionPreference
        try {
            $ErrorActionPreference = "SilentlyContinue"
            & $ContainerEngine compose -f $ComposeFile exec -T dind sh -c 'docker ps -aq 2>/dev/null | xargs docker rm -f 2>/dev/null' *>$null
            & $ContainerEngine compose -f $ComposeFile down --remove-orphans *>$null
        } finally {
            $ErrorActionPreference = $prevEAP
        }
    }
}

# ── Preflight: is a container engine usable? ──────────────────────
# Attempts to connect to the engine and auto-starts Docker Desktop if needed.
# Exits with error if engine is not reachable.
function Ensure-EngineRunning {
    if (-not $ContainerEngine) {
        Write-Host "Error: Neither docker nor podman found in PATH." -ForegroundColor Red
        Write-Host ""
        Write-Host "Install one of:" -ForegroundColor Yellow
        Write-Host "  Docker : https://docs.docker.com/get-docker/"
        Write-Host "  Podman : https://podman.io/getting-started/installation"
        Remove-SandboxTmpDir; exit 1
    }

    $_prevEAPInfo = $ErrorActionPreference
    try {
        $ErrorActionPreference = "SilentlyContinue"
        $null = & $ContainerEngine info 2>&1
        $_infoExit = $LASTEXITCODE
    } finally {
        $ErrorActionPreference = $_prevEAPInfo
    }

    if ($_infoExit -ne 0) {
        # Attempt to auto-start Docker Desktop if installed but not running
        $DockerStarted = $false
        if ($ContainerEngine -eq "docker") {
            $DockerDesktopPath = "$env:ProgramFiles\Docker\Docker\Docker Desktop.exe"
            if (Test-Path $DockerDesktopPath) {
                Write-Host "Docker Desktop is not running. Starting it..." -ForegroundColor Yellow
                Start-Process $DockerDesktopPath
                $Timeout = 60
                $Elapsed = 0
                $_prevEAPStartup = $ErrorActionPreference
                try {
                    $ErrorActionPreference = "SilentlyContinue"
                    while ($Elapsed -lt $Timeout) {
                        Start-Sleep -Seconds 2
                        $Elapsed += 2
                        $null = & $ContainerEngine info 2>&1
                        if ($LASTEXITCODE -eq 0) {
                            Write-Host "Docker Desktop is ready." -ForegroundColor Green
                            $DockerStarted = $true
                            break
                        }
                        Write-Host "  Waiting for Docker Desktop to start... ($Elapsed/$Timeout`s)" -ForegroundColor DarkGray
                    }
                } finally {
                    $ErrorActionPreference = $_prevEAPStartup
                }
                if (-not $DockerStarted) {
                    Write-Host "Error: Docker Desktop did not become ready within $Timeout seconds." -ForegroundColor Red
                }
            }
        }
        if (-not $DockerStarted) {
            Write-Host "Error: Cannot connect to $ContainerEngine." -ForegroundColor Red
            Write-Host ""
            if ($ContainerEngine -eq "docker") {
                Write-Host "On Windows, make sure:" -ForegroundColor Yellow
                Write-Host "  1. Docker Desktop is installed and running."
                Write-Host "  2. Your user is in the 'docker-users' group."
                Write-Host "     An admin can add you with:"
                Write-Host "       net localgroup docker-users $env:USERNAME /add"
                Write-Host "     Then log out and back in."
            } else {
                Write-Host "Make sure the Podman machine is running:" -ForegroundColor Yellow
                Write-Host "  podman machine start"
            }
            Remove-SandboxTmpDir; exit 1
        }
    }
}

# ── Resolve resource limits ───────────────────────────────────────
# Resolves memory and CPU limits from "auto", percentage, or absolute values.
# Sets $script:ResolvedMemory, $script:ResolvedCpus, $script:ResolvedMemorySwap.
function Resolve-Resources {
    $script:ResolvedMemory = ""
    $script:ResolvedCpus = ""
    $script:ResolvedMemorySwap = $null

    # Query engine resources once (used by auto and percentage cap)
    $engineMemGB = 0
    $engineCpus  = 0
    try {
        $engineMemStr = & $ContainerEngine info --format '{{.MemTotal}}' 2>$null
        if ($engineMemStr -match '^\d+$') {
            $engineMemGB = [math]::Floor([long]$engineMemStr / 1073741824)
        }
        $engineCpuStr = & $ContainerEngine info --format '{{.NCPU}}' 2>$null
        if ($engineCpuStr -match '^\d+$') {
            $engineCpus = [int]$engineCpuStr
        }
    } catch {}

    # ── Memory resolution ─────────────────────────────────────────
    if ($MemoryLimit -eq 'auto') {
        if ($engineMemGB -gt 0) {
            # Reserve 2g for system (Docker daemon, kernel, page cache, WSL2 overhead)
            $memUsable = $engineMemGB - 2
            if ($memUsable -lt 2) { $memUsable = 2 }
            # Count running DevCage containers with auto memory for fair-share allocation
            $memAutoIds = @()
            try {
                $ids = & $ContainerEngine ps --filter "label=dev-sandbox.memory-mode=auto" --format '{{.ID}}' 2>$null
                if ($ids) { $memAutoIds = @($ids | Where-Object { $_ -ne '' }) }
            } catch {}
            $autoMem = [math]::Floor($memUsable / ($memAutoIds.Count + 1))
            # Enforce 4g minimum for agentic coding workloads
            $memOvercommit = $false
            if ($autoMem -lt 4) {
                $autoMem = 4
                # Only warn if total allocation across all containers exceeds usable
                $totalAlloc = 4 * ($memAutoIds.Count + 1)
                if ($totalAlloc -gt $memUsable) {
                    $memOvercommit = $true
                }
            }
            $script:ResolvedMemory = "${autoMem}g"
            # Swap: 50% of memory (capped at 4g)
            $swapExtra = [math]::Min([math]::Floor($autoMem / 2), 4)
            $memswapTotal = $autoMem + $swapExtra
            $script:ResolvedMemorySwap = "${memswapTotal}g"
            # Rebalance existing auto-memory containers to the same fair share
            foreach ($cid in $memAutoIds) {
                & $ContainerEngine update --memory "${autoMem}g" --memory-swap "${memswapTotal}g" $cid *>$null
            }
            # Warn when floor exceeds fair share
            if ($memOvercommit) {
                $totalContainers = $memAutoIds.Count + 1
                Write-Host "Warning: memory over-committed -- ${totalContainers} containers x ${autoMem}g = $($totalContainers * $autoMem)g, but only ${memUsable}g usable (${engineMemGB}g engine - 2g overhead)." -ForegroundColor Yellow
                Write-Host "  Increase Docker Desktop memory, stop idle containers, or set an explicit --memory limit." -ForegroundColor Yellow
            }
        } else {
            Write-Host "Warning: cannot query engine memory, falling back to 8g" -ForegroundColor Yellow
            $script:ResolvedMemory = "8g"
        }
    } elseif ($MemoryLimit -match '^(\d+)%$') {
        $memPct = [int]$Matches[1]
        if ($memPct -lt 1 -or $memPct -gt 100) {
            Write-Host "Error: --memory percentage must be 1-100 (got '${MemoryLimit}')" -ForegroundColor Red
            Remove-SandboxTmpDir; exit 1
        }
        $totalBytes = (Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory
        $memTotalGB = [math]::Floor($totalBytes / 1073741824)
        $resultGB = [math]::Floor($memTotalGB * $memPct / 100)
        if ($resultGB -lt 1) { $resultGB = 1 }
        # Cap to engine's available memory (Docker Desktop VM may have less than host)
        if ($engineMemGB -gt 0 -and $resultGB -gt $engineMemGB) {
            $resultGB = $engineMemGB
        }
        $script:ResolvedMemory = "${resultGB}g"
    } else {
        if ($MemoryLimit -notmatch '^[0-9]+[kmgKMG]?$') {
            Write-Host "Error: invalid --memory value '${MemoryLimit}' (expected e.g. 8g, 4096m, 50%, auto)" -ForegroundColor Red
            Remove-SandboxTmpDir; exit 1
        }
        $script:ResolvedMemory = $MemoryLimit
    }

    # ── CPU resolution ────────────────────────────────────────────
    if ($CpuLimit -eq 'auto') {
        if ($engineCpus -gt 0) {
            # Reserve 2 CPUs for system (Docker daemon, kernel)
            $cpuUsable = $engineCpus - 2
            if ($cpuUsable -lt 1) { $cpuUsable = 1 }
            # Count running DevCage containers with auto CPUs for fair-share allocation
            $cpuAutoIds = @()
            try {
                $ids = & $ContainerEngine ps --filter "label=dev-sandbox.cpu-mode=auto" --format '{{.ID}}' 2>$null
                if ($ids) { $cpuAutoIds = @($ids | Where-Object { $_ -ne '' }) }
            } catch {}
            $autoCpus = [math]::Floor($cpuUsable / ($cpuAutoIds.Count + 1))
            if ($autoCpus -lt 1) { $autoCpus = 1 }
            $script:ResolvedCpus = "$autoCpus"
            # Rebalance existing auto-CPU containers to the same fair share
            foreach ($cid in $cpuAutoIds) {
                & $ContainerEngine update --cpus $autoCpus $cid *>$null
            }
        } else {
            Write-Host "Warning: cannot query engine CPUs, falling back to 2" -ForegroundColor Yellow
            $script:ResolvedCpus = "2"
        }
    } elseif ($CpuLimit -match '^(\d+)%$') {
        $cpuPct = [int]$Matches[1]
        if ($cpuPct -lt 1 -or $cpuPct -gt 100) {
            Write-Host "Error: --cpus percentage must be 1-100 (got '${CpuLimit}')" -ForegroundColor Red
            Remove-SandboxTmpDir; exit 1
        }
        $cpuTotal = (Get-CimInstance Win32_Processor | Measure-Object -Property NumberOfLogicalProcessors -Sum).Sum
        if (-not $cpuTotal) { $cpuTotal = [int]$env:NUMBER_OF_PROCESSORS }
        $resultCpus = [math]::Round($cpuTotal * $cpuPct / 100, [MidpointRounding]::AwayFromZero)
        if ($resultCpus -lt 1) { $resultCpus = 1 }
        # Cap to engine's available CPUs
        if ($engineCpus -gt 0 -and $resultCpus -gt $engineCpus) {
            $resultCpus = $engineCpus
        }
        $script:ResolvedCpus = "$resultCpus"
    } else {
        if ($CpuLimit -notmatch '^[0-9]+(\.[0-9]+)?$') {
            Write-Host "Error: invalid --cpus value '${CpuLimit}' (expected e.g. 4, 2.5, 50%, auto)" -ForegroundColor Red
            Remove-SandboxTmpDir; exit 1
        }
        $script:ResolvedCpus = $CpuLimit
    }
}

# ── Export resource environment variables ─────────────────────────
# Sets $env:SANDBOX_MEMORY, $env:SANDBOX_MEMORY_SWAP, $env:SANDBOX_CPUS,
# $env:DIND_MEMORY, $env:DIND_CPUS.
function Export-ResourceEnv {
    $env:SANDBOX_MEMORY = $ResolvedMemory
    $env:SANDBOX_CPUS   = $ResolvedCpus

    # Swap limit: computed in auto mode, 50% of memory for explicit values
    if ($ResolvedMemorySwap) {
        $env:SANDBOX_MEMORY_SWAP = $ResolvedMemorySwap
    } elseif ($ResolvedMemory -match '^(\d+)g$') {
        $sw = [math]::Min([math]::Floor([int]$Matches[1] / 2), 4)
        $env:SANDBOX_MEMORY_SWAP = "$([int]$Matches[1] + $sw)g"
    } else {
        $env:SANDBOX_MEMORY_SWAP = $ResolvedMemory
    }

    # DinD sidecar gets half the workspace limits (minimum 1g / 1 cpu).
    $dindMemory = $ResolvedMemory
    if ($ResolvedMemory -match '^(\d+)g$') {
        $dindGb = [math]::Max(1, [math]::Floor([int]$Matches[1] / 2))
        $dindMemory = "${dindGb}g"
    }
    $env:DIND_MEMORY = $dindMemory

    $dindCpus = $ResolvedCpus
    if ($ResolvedCpus -match '^\d+$') {
        $dindN = [math]::Max(1, [math]::Floor([int]$ResolvedCpus / 2))
        $dindCpus = "$dindN"
    }
    $env:DIND_CPUS = $dindCpus
}
