# Copyright (c) 2026 Progralink. Licensed under the DevCage License — see LICENSE.
# shellcheck shell=bash

# resources.sh — Resource management functions for devcage.sh.
# Depends on: CONTAINER_ENGINE, MEMORY_LIMIT, CPU_LIMIT, NO_GPU, GPU_TYPE,
#             EMBED_DOCKER, SKIP_SANDBOX_WARNING (globals set by caller).

# ── Detect container engine (docker preferred, podman fallback) ───
_detect_engine() {
    if command -v docker >/dev/null 2>&1; then
        CONTAINER_ENGINE="docker"
    elif command -v podman >/dev/null 2>&1; then
        CONTAINER_ENGINE="podman"
    else
        CONTAINER_ENGINE=""
    fi
}

# ── Ensure engine is running; auto-start Docker Desktop on macOS ──
# Exits with error if the engine is not usable.
_ensure_engine_running() {
    if [ "$CONTAINER_ENGINE" = "" ]; then
        echo "Error: Neither docker nor podman found in PATH." >&2
        echo "" >&2
        echo "Install one of:" >&2
        echo "  Docker : https://docs.docker.com/get-docker/" >&2
        echo "  Podman : https://podman.io/getting-started/installation" >&2
        exit 1
    fi

    if ! "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
        # Attempt to auto-start Docker Desktop if installed but not running
        DOCKER_STARTED=0
        if [ "$CONTAINER_ENGINE" = "docker" ] && [ "$(uname -s)" = "Darwin" ]; then
            if [ -d "/Applications/Docker.app" ]; then
                echo "Docker Desktop is not running. Starting it..." >&2
                open -a Docker
                TIMEOUT=60
                ELAPSED=0
                while [ "$ELAPSED" -lt "$TIMEOUT" ]; do
                    sleep 2
                    ELAPSED=$((ELAPSED + 2))
                    if "$CONTAINER_ENGINE" info >/dev/null 2>&1; then
                        echo "Docker Desktop is ready." >&2
                        DOCKER_STARTED=1
                        break
                    fi
                    echo "  Waiting for Docker Desktop to start... (${ELAPSED}/${TIMEOUT}s)" >&2
                done
                if [ "$DOCKER_STARTED" = "0" ]; then
                    echo "Error: Docker Desktop did not become ready within ${TIMEOUT} seconds." >&2
                fi
            fi
        fi
        if [ "$DOCKER_STARTED" = "0" ]; then
            echo "Error: Cannot connect to ${CONTAINER_ENGINE}." >&2
            echo "" >&2
            if [ "$CONTAINER_ENGINE" = "docker" ]; then
                case "$(uname -s)" in
                    Linux)
                        echo "On Linux, non-root users need to be in the 'docker' group:" >&2
                        echo "  sudo usermod -aG docker \$USER" >&2
                        echo "  # then log out and back in (or: newgrp docker)" >&2
                        echo "" >&2
                        echo "Alternatively, install rootless Docker:" >&2
                        echo "  https://docs.docker.com/engine/security/rootless/" >&2
                        ;;
                    Darwin)
                        echo "On macOS, make sure Docker Desktop is running." >&2
                        echo "Any user can use it once it's started — no admin needed." >&2
                        ;;
                    *)
                        echo "Please start Docker Desktop or ensure the Docker daemon is running." >&2
                        ;;
                esac
            else
                echo "Make sure the Podman machine is running:" >&2
                echo "  podman machine start" >&2
            fi
            exit 1
        fi
    fi
}

# ── Resolve resource limits ───────────────────────────────────────
# "auto" = use whatever the container engine reports (Docker Desktop VM limits).
# Percentage values (e.g. 50%) are calculated from host RAM/CPUs, then capped
# to the engine's available resources to avoid over-allocation.
# Sets: RESOLVED_MEMORY, RESOLVED_MEMORY_SWAP, RESOLVED_CPUS, MEMORY_TOTAL_GB
_resolve_resources() {
    MEMORY_TOTAL_GB=""

    # Query engine resources once (used by auto and percentage cap)
    _engine_mem_gb=""
    _engine_cpus=""
    if [ "$CONTAINER_ENGINE" != "" ]; then
        _engine_info="$("$CONTAINER_ENGINE" info --format '{{.MemTotal}} {{.NCPU}}' 2>/dev/null || true)"
        if [[ "$_engine_info" =~ ^([0-9]+)\ ([0-9]+)$ ]]; then
            _engine_mem_gb=$(( 10#${BASH_REMATCH[1]} / 1073741824 ))
            _engine_cpus="${BASH_REMATCH[2]}"
        fi
    fi

    if [[ "$MEMORY_LIMIT" == "auto" ]]; then
        if [[ -n "$_engine_mem_gb" ]] && [ "$_engine_mem_gb" -gt 0 ]; then
            MEMORY_TOTAL_GB="$_engine_mem_gb"
            # Reserve 2g for system (Docker daemon, kernel, page cache, WSL2 overhead)
            _mem_usable=$(( _engine_mem_gb - 2 ))
            [ "$_mem_usable" -lt 2 ] && _mem_usable=2
            # Count running DevCage containers with auto memory for fair-share allocation
            _mem_auto_ids=()
            while IFS= read -r _cid; do
                [ "$_cid" != "" ] && _mem_auto_ids+=("$_cid")
            done < <("$CONTAINER_ENGINE" ps --filter "label=dev-sandbox.memory-mode=auto" --format '{{.ID}}' 2>/dev/null || true)
            _auto_mem=$(( _mem_usable / (${#_mem_auto_ids[@]} + 1) ))
            # Enforce 4g minimum for agentic coding workloads
            _mem_overcommit=0
            if [ "$_auto_mem" -lt 4 ]; then
                _auto_mem=4
                # Only warn if total allocation across all containers exceeds usable
                _total_alloc=$(( 4 * (${#_mem_auto_ids[@]} + 1) ))
                if [ "$_total_alloc" -gt "$_mem_usable" ]; then
                    _mem_overcommit=1
                fi
            fi
            RESOLVED_MEMORY="${_auto_mem}g"
            # Swap: 50% of memory (capped at 4g) — enough for brief build peaks,
            # not enough for prolonged thrash that burns API tokens doing nothing
            _swap_extra=$(( _auto_mem / 2 ))
            [ "$_swap_extra" -gt 4 ] && _swap_extra=4
            _memswap_total=$(( _auto_mem + _swap_extra ))
            RESOLVED_MEMORY_SWAP="${_memswap_total}g"
            # Rebalance existing auto-memory containers to the same fair share
            if [ ${#_mem_auto_ids[@]} -gt 0 ]; then
                for _cid in "${_mem_auto_ids[@]}"; do
                    "$CONTAINER_ENGINE" update --memory "${_auto_mem}g" --memory-swap "${_memswap_total}g" "$_cid" >/dev/null 2>&1 || true
                done
            fi
            # Warn when floor exceeds fair share (engine is too small for this many containers)
            if [ "$_mem_overcommit" = "1" ]; then
                _total_containers=$(( ${#_mem_auto_ids[@]} + 1 ))
                _total_requested=$(( _total_containers * _auto_mem ))
                echo "Warning: memory over-committed — ${_total_containers} containers x ${_auto_mem}g = ${_total_requested}g, but only ${_mem_usable}g usable (${_engine_mem_gb}g engine - 2g overhead)." >&2
                echo "  Increase Docker Desktop memory, stop idle containers, or set an explicit --memory limit." >&2
            fi
        else
            echo "Warning: cannot query engine memory, falling back to 8g" >&2
            RESOLVED_MEMORY="8g"
        fi
    elif [[ "$MEMORY_LIMIT" == *% ]]; then
        _mem_pct="${MEMORY_LIMIT%\%}"
        if ! [[ "$_mem_pct" =~ ^[0-9]+$ ]] || [ "$_mem_pct" -lt 1 ] || [ "$_mem_pct" -gt 100 ]; then
            echo "Error: --memory percentage must be 1-100 (got '${MEMORY_LIMIT}')" >&2
            exit 1
        fi
        case "$(uname -s)" in
            Darwin) _mem_bytes="$(sysctl -n hw.memsize)" ;;
            Linux)  _mem_bytes="$(awk '/MemTotal/ {printf "%.0f", $2 * 1024}' /proc/meminfo)" ;;
            *)      echo "Warning: cannot detect memory on $(uname -s), pass an absolute value" >&2
                    _mem_bytes="" ;;
        esac
        if [[ -n "$_mem_bytes" ]]; then
            MEMORY_TOTAL_GB=$(( _mem_bytes / 1073741824 ))
            _mem_result=$(( MEMORY_TOTAL_GB * _mem_pct / 100 ))
            [ "$_mem_result" -lt 1 ] && _mem_result=1
            # Cap to engine's available memory (Docker Desktop VM may have less than host)
            if [[ -n "$_engine_mem_gb" ]] && [ "$_engine_mem_gb" -gt 0 ] && [ "$_mem_result" -gt "$_engine_mem_gb" ]; then
                _mem_result="$_engine_mem_gb"
            fi
            RESOLVED_MEMORY="${_mem_result}g"
        else
            RESOLVED_MEMORY="$MEMORY_LIMIT"
        fi
    else
        if ! [[ "$MEMORY_LIMIT" =~ ^[0-9]+[kmgKMG]?$ ]]; then
            echo "Error: invalid --memory value '${MEMORY_LIMIT}' (expected e.g. 8g, 4096m, 50%, auto)" >&2
            exit 1
        fi
        RESOLVED_MEMORY="$MEMORY_LIMIT"
    fi

    if [[ "$CPU_LIMIT" == "auto" ]]; then
        if [[ -n "$_engine_cpus" ]] && [ "$_engine_cpus" -gt 0 ]; then
            # Reserve 2 CPUs for system (Docker daemon, kernel)
            _cpu_usable=$(( _engine_cpus - 2 ))
            [ "$_cpu_usable" -lt 1 ] && _cpu_usable=1
            # Count running DevCage containers with auto CPUs for fair-share allocation
            _cpu_auto_ids=()
            while IFS= read -r _cid; do
                [ "$_cid" != "" ] && _cpu_auto_ids+=("$_cid")
            done < <("$CONTAINER_ENGINE" ps --filter "label=dev-sandbox.cpu-mode=auto" --format '{{.ID}}' 2>/dev/null || true)
            _auto_cpus=$(( _cpu_usable / (${#_cpu_auto_ids[@]} + 1) ))
            [ "$_auto_cpus" -lt 1 ] && _auto_cpus=1
            RESOLVED_CPUS="$_auto_cpus"
            # Rebalance existing auto-CPU containers to the same fair share
            if [ ${#_cpu_auto_ids[@]} -gt 0 ]; then
                for _cid in "${_cpu_auto_ids[@]}"; do
                    "$CONTAINER_ENGINE" update --cpus "$_auto_cpus" "$_cid" >/dev/null 2>&1 || true
                done
            fi
        else
            echo "Warning: cannot query engine CPUs, falling back to 2" >&2
            RESOLVED_CPUS="2"
        fi
    elif [[ "$CPU_LIMIT" == *% ]]; then
        _cpu_pct="${CPU_LIMIT%\%}"
        if ! [[ "$_cpu_pct" =~ ^[0-9]+$ ]] || [ "$_cpu_pct" -lt 1 ] || [ "$_cpu_pct" -gt 100 ]; then
            echo "Error: --cpus percentage must be 1-100 (got '${CPU_LIMIT}')" >&2
            exit 1
        fi
        case "$(uname -s)" in
            Darwin) _cpu_total="$(sysctl -n hw.ncpu)" ;;
            Linux)  _cpu_total="$(nproc)" ;;
            *)      echo "Warning: cannot detect CPUs on $(uname -s), pass an absolute value" >&2
                    _cpu_total="" ;;
        esac
        if [[ -n "$_cpu_total" ]]; then
            _cpu_result=$(( (_cpu_total * _cpu_pct + 50) / 100 ))
            [ "$_cpu_result" -lt 1 ] && _cpu_result=1
            # Cap to engine's available CPUs (Docker Desktop VM may have fewer than host)
            if [[ -n "$_engine_cpus" ]] && [ "$_engine_cpus" -gt 0 ] && [ "$_cpu_result" -gt "$_engine_cpus" ]; then
                _cpu_result="$_engine_cpus"
            fi
            RESOLVED_CPUS="$_cpu_result"
        else
            RESOLVED_CPUS="$CPU_LIMIT"
        fi
    else
        if ! [[ "$CPU_LIMIT" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
            echo "Error: invalid --cpus value '${CPU_LIMIT}' (expected e.g. 4, 2.5, 50%, auto)" >&2
            exit 1
        fi
        RESOLVED_CPUS="$CPU_LIMIT"
    fi
}

# ── GPU auto-detection ────────────────────────────────────────────
# Also handles Podman auto-skip of DinD.
# Sets: GPU_TYPE, EMBED_DOCKER
_detect_gpu() {
    # Auto-skip DinD sidecar when using Podman (Docker-specific image)
    if [ "$CONTAINER_ENGINE" = "podman" ] && [ "$EMBED_DOCKER" = "1" ]; then
        echo "Note: Podman detected — auto-skipping Docker-in-Docker sidecar." >&2
        EMBED_DOCKER=0
    fi

    # Skip on macOS (Docker Desktop VM has no GPU access).
    if [ "$NO_GPU" = "0" ] && [ "$GPU_TYPE" != "" ] && [ "$GPU_TYPE" != "auto" ] && [ "$(uname -s)" = "Darwin" ]; then
        echo "Warning: GPU passthrough is not supported on macOS (Docker Desktop VM limitation) — ignoring --gpu" >&2
        GPU_TYPE=""
    fi
    # Auto-detect if not already set to a specific type (includes --gpu without type → "auto").
    if [ "$NO_GPU" = "0" ] && { [ "$GPU_TYPE" = "" ] || [ "$GPU_TYPE" = "auto" ]; } && [ "$(uname -s)" != "Darwin" ]; then
        if [ "$CONTAINER_ENGINE" = "docker" ]; then
            _runtimes="$("$CONTAINER_ENGINE" info --format '{{json .Runtimes}}' 2>/dev/null || true)"
            if echo "$_runtimes" | grep -q '"nvidia"'; then
                GPU_TYPE="nvidia"
            elif echo "$_runtimes" | grep -q '"amd"'; then
                GPU_TYPE="amd"
            fi
        elif [ "$CONTAINER_ENGINE" = "podman" ]; then
            if command -v nvidia-ctk >/dev/null 2>&1; then
                GPU_TYPE="nvidia"
            elif [ -e /dev/kfd ]; then
                GPU_TYPE="amd"
            fi
        fi
    fi
    # --gpu without a type triggers auto-detect; clear "auto" if nothing found
    if [ "$GPU_TYPE" = "auto" ]; then
        _warn "--gpu requested but no GPU runtime detected — proceeding without GPU"
        GPU_TYPE=""
    fi
}

# ── Host environment setup ────────────────────────────────────────
# Sets HOST_UID, _HOST_OS, defines _host_path(), sets MSYS_NO_PATHCONV on Windows.
# Also emits the Linux sandbox security warning when applicable.
# Depends on: CONTAINER_ENGINE, SKIP_SANDBOX_WARNING
_setup_host_env() {
    # ── Linux sandbox security warning ───────────────────────────────
    # On Linux, containers share the host kernel (no VM boundary like Docker
    # Desktop / Podman Desktop on macOS and Windows). Detect whether the engine
    # is running inside a VM and warn if not.
    if [ "$(uname -s)" = "Linux" ] && [ "$SKIP_SANDBOX_WARNING" = "0" ]; then
        _VM_ISOLATED=0
        if [ "$CONTAINER_ENGINE" = "docker" ]; then
            # Docker Desktop for Linux reports "Docker Desktop" in OperatingSystem
            _os_field="$(docker info --format '{{.OperatingSystem}}' 2>/dev/null || true)"
            case "$_os_field" in
                *"Docker Desktop"*) _VM_ISOLATED=1 ;;
            esac
        elif [ "$CONTAINER_ENGINE" = "podman" ]; then
            # If Podman is talking to a machine VM, `podman machine list` shows
            # a currently-running machine. Native Podman has no machine at all.
            if podman machine list --format '{{.Running}}' 2>/dev/null | grep -qi 'true'; then
                _VM_ISOLATED=1
            fi
        fi
        if [ "$_VM_ISOLATED" = "0" ]; then
            _info ""
            _info "╔══════════════════════════════════════════════════════════════════╗"
            _info "║                     ⚠  SECURITY WARNING ⚠                       ║"
            _info "╠══════════════════════════════════════════════════════════════════╣"
            _info "║                                                                  ║"
            _info "║  You are running on Linux without VM-backed container isolation. ║"
            _info "║                                                                  ║"
            _info "║  On macOS/Windows, Docker Desktop and Podman Desktop run         ║"
            _info "║  containers inside a Linux VM, providing a strong security       ║"
            _info "║  boundary. On Linux, containers share the host kernel directly.  ║"
            _info "║                                                                  ║"
            _info "║  DevCage still applies: dropped capabilities, no-new-privileges, ║"
            _info "║  unprivileged user, and LAN blocking — but a kernel exploit      ║"
            _info "║  could escape the container to the host.                         ║"
            _info "║                                                                  ║"
            _info "║  For stronger isolation, consider:                               ║"
            _info "║    • Docker Desktop for Linux (runs containers in a VM)          ║"
            _info "║    • Podman with 'podman machine' (QEMU VM)                      ║"
            _info "║                                                                  ║"
            _info "║  To suppress this warning:                                       ║"
            _info "║    --skip-sandbox-warning  or  SKIP_SANDBOX_WARNING=1            ║"
            _info "║    in config/switches.conf                                        ║"
            _info "║                                                                  ║"
            _info "╚══════════════════════════════════════════════════════════════════╝"
            _info ""
        fi
    fi

    # ── Host user UID (so container files match host ownership) ──────
    export HOST_UID
    HOST_UID="$(id -u)"

    # ── Host OS detection (entrypoint uses this for platform-specific optimizations) ──
    case "$(uname -s)" in
        Darwin)                _HOST_OS="macos" ;;
        MINGW*|MSYS*|CYGWIN*) _HOST_OS="windows" ;;
        *)                     _HOST_OS="linux" ;;
    esac

    # ── MSYS path conversion (Git Bash / MSYS2 on Windows) ──────────────────────
    # Git Bash converts arguments starting with '/' to Windows paths before passing
    # them to child processes (e.g. /workspace/... → C:/Program Files/Git/workspace/...).
    # Docker Desktop's daemon (WSL2 backend) handles Git Bash host paths natively,
    # so disabling MSYS conversion is safe for docker run/exec volume sources and
    # prevents container-side paths from being mangled in --mount targets, -e env
    # var values, and command arguments.
    # EXCEPTION: Docker CLI file APIs (docker compose -f, docker compose cp) run on
    # the Windows host and require Windows-format paths. Use _host_path() to convert
    # those specific paths with cygpath before passing them to Docker CLI.
    # shellcheck disable=SC2317  # function bodies in if/else are reachable at runtime
    if [ "$_HOST_OS" = "windows" ]; then
        export MSYS_NO_PATHCONV=1
        export MSYS2_ARG_CONV_EXCL="*"
        _host_path() { cygpath -m "$1" 2>/dev/null || echo "$1"; }
    else
        _host_path() { echo "$1"; }
    fi
}

# ── Export resource env vars for docker-compose.yml ──────────────
# Exports SANDBOX_MEMORY, SANDBOX_CPUS, SANDBOX_MEMORY_SWAP, DIND_MEMORY, DIND_CPUS.
# These env vars are consumed by docker-compose.yml.
# Depends on: RESOLVED_MEMORY, RESOLVED_CPUS, RESOLVED_MEMORY_SWAP (from _resolve_resources)
_export_resource_env() {
    # ── Resource limits (consumed by docker-compose.yml via env vars) ─
    export SANDBOX_MEMORY="$RESOLVED_MEMORY"
    export SANDBOX_CPUS="$RESOLVED_CPUS"

    # Swap limit: computed in auto mode, 50% of memory for explicit values
    if [[ -n "${RESOLVED_MEMORY_SWAP:-}" ]]; then
        export SANDBOX_MEMORY_SWAP="$RESOLVED_MEMORY_SWAP"
    elif [[ "$RESOLVED_MEMORY" =~ ^([0-9]+)g$ ]]; then
        _sw=$(( BASH_REMATCH[1] / 2 ))
        [ "$_sw" -gt 4 ] && _sw=4
        export SANDBOX_MEMORY_SWAP="$(( BASH_REMATCH[1] + _sw ))g"
    else
        export SANDBOX_MEMORY_SWAP="$RESOLVED_MEMORY"
    fi

    # DinD sidecar gets half the workspace limits (minimum 1g / 1 cpu).
    _dind_mem="$RESOLVED_MEMORY"
    if [[ "$RESOLVED_MEMORY" =~ ^([0-9]+)g$ ]]; then
        _dind_gb=$(( BASH_REMATCH[1] / 2 ))
        [ "$_dind_gb" -lt 1 ] && _dind_gb=1
        _dind_mem="${_dind_gb}g"
    fi
    export DIND_MEMORY="$_dind_mem"

    _dind_cpus="$RESOLVED_CPUS"
    if [[ "$RESOLVED_CPUS" =~ ^([0-9]+)$ ]]; then
        _dind_n=$(( BASH_REMATCH[1] / 2 ))
        [ "$_dind_n" -lt 1 ] && _dind_n=1
        _dind_cpus="$_dind_n"
    fi
    export DIND_CPUS="$_dind_cpus"
}
