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

# utils.sh — Utility functions for devcage.sh.
# Depends on: _BLOCKED_DESTINATIONS and _INTERACTIVE_COMMANDS (from constants.sh),
#             _ALL_LANGS (from constants.sh), _sha256 (defined below).

# ── Validate an IPv4 address (each octet 0-255) ──────────────────
_valid_ipv4() {
    local ip="$1"
    [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || return 1
    IFS='.' read -r a b c d <<< "$ip"
    (( a <= 255 && b <= 255 && c <= 255 && d <= 255 ))
}

# ── Portable SHA-256 hash: prefers sha256sum (Linux/GNU), falls back to shasum (macOS) ──
_sha256() {
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum
    elif command -v shasum >/dev/null 2>&1; then
        shasum -a 256
    else
        openssl dgst -sha256 -r
    fi
}

# ── Normalize workspace path (tilde expansion + resolve + exists) ─
_normalize_workspace_path() {
    local _raw="$1"
    # Tilde expansion
    case "$_raw" in
        "~"/*) _raw="${HOME}${_raw#"~"}" ;;
        "~")   _raw="$HOME" ;;
    esac
    # Resolve to absolute path
    local _abs
    _abs="$(cd "$_raw" 2>/dev/null && pwd)" || {
        echo "Error: workspace directory does not exist: $_raw" >&2
        exit 1
    }
    printf '%s' "$_abs"
}

# ── Validate a custom volume spec (SOURCE:DEST[:MODE]) ───────────
# Prints validated SOURCE:DEST:MODE on success; exits with error on failure.
_validate_volume_spec() {
    local _spec="$1"
    local _src _dest _mode="ro"

    # Split on colon — at most 3 parts (SOURCE:DEST:MODE)
    IFS=':' read -ra _parts <<< "$_spec"
    local _nparts=${#_parts[@]}

    if [ "$_nparts" -lt 2 ] || [ "$_nparts" -gt 3 ]; then
        echo "Error: invalid volume spec '${_spec}' — expected SOURCE:DEST[:MODE]" >&2
        return 1
    fi

    _src="${_parts[0]}"
    _dest="${_parts[1]}"
    [ "$_nparts" -eq 3 ] && _mode="${_parts[2]}"

    # Tilde expansion on source
    case "$_src" in
        "~"/*) _src="${HOME}${_src#"~"}" ;;
        "~")   _src="$HOME" ;;
    esac

    # Resolve source to absolute path and check existence
    if [ ! -e "$_src" ]; then
        echo "Error: volume source does not exist: ${_src}" >&2
        return 1
    fi
    _src="$(cd "$_src" 2>/dev/null && pwd)" || {
        # Not a directory — resolve parent + basename
        local _parent _base
        _parent="$(cd "$(dirname "$_src")" 2>/dev/null && pwd)" || {
            echo "Error: cannot resolve volume source path: ${_src}" >&2
            return 1
        }
        _base="$(basename "$_src")"
        _src="${_parent}/${_base}"
    }

    # Validate destination is absolute
    if [[ "$_dest" != /* ]]; then
        echo "Error: volume destination must be an absolute path (got '${_dest}')" >&2
        return 1
    fi

    # Normalize destination: remove trailing slashes and resolve ../ components
    # to prevent path-traversal bypass of blocked destinations.
    _dest="${_dest%/}"
    [ "$_dest" = "" ] && _dest="/"
    # Collapse double slashes to prevent bypass of blocked destinations
    while [[ "$_dest" == *//* ]]; do _dest="${_dest//\/\//\/}"; done
    # Canonicalize: resolve . and .. segments (container path, can't use cd+pwd)
    if [[ "$_dest" == *".."* || "$_dest" == *"/."* ]]; then
        local _parts _canon=()
        IFS='/' read -ra _parts <<< "$_dest"
        for _seg in "${_parts[@]}"; do
            case "$_seg" in
                ""|".") ;;
                "..") [ ${#_canon[@]} -gt 0 ] && unset '_canon[${#_canon[@]}-1]' ;;
                *) _canon+=("$_seg") ;;
            esac
        done
        _dest=""
        for _seg in "${_canon[@]+"${_canon[@]}"}"; do
            _dest="${_dest}/${_seg}"
        done
        [ "$_dest" = "" ] && _dest="/"
    fi

    # Check against blocked destinations (prefix match)
    for _blocked in "${_BLOCKED_DESTINATIONS[@]}"; do
        if [ "$_dest" = "$_blocked" ] || [[ "$_dest" == "${_blocked}/"* ]]; then
            echo "Error: volume destination '${_dest}' is blocked (conflicts with '${_blocked}')" >&2
            return 1
        fi
    done

    # Validate mode
    case "$_mode" in
        ro|rw) ;;
        *)
            echo "Error: volume mode must be 'ro' or 'rw' (got '${_mode}')" >&2
            return 1
            ;;
    esac

    printf '%s:%s:%s' "$_src" "$_dest" "$_mode"
}

# ── Resolve a language name to its canonical form ────────────────
_resolve_lang() {
    case "$(echo "$1" | tr '[:upper:]' '[:lower:]')" in
        java)                           echo "java" ;;
        php)                            echo "php" ;;
        go|golang)                      echo "go" ;;
        rust)                           echo "rust" ;;
        cpp|c++|c)                      echo "cpp" ;;
        python|py)                      echo "python" ;;
        javascript|typescript|js|ts)    echo "javascript" ;;
        dotnet|.net|csharp|c\#|vb|visualbasic|f\#|fsharp)  echo "dotnet" ;;
        *)                              echo "" ;;
    esac
}

# ── Build --build-arg flags for selected languages ────────────────
_lang_build_args() {
    local _langs_csv="$1"
    local _enabled=" "
    IFS=',' read -ra _items <<< "$_langs_csv"
    local _first; _first="$(echo "${_items[0]}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
    if [ ${#_items[@]} -eq 1 ] && [ "$_first" = "all" ]; then
        return  # all languages enabled — no DEV_*=false args
    fi
    if [ ${#_items[@]} -eq 1 ] && [ "$_first" = "none" ]; then
        for _lang in "${_ALL_LANGS[@]}"; do
            local _upper; _upper="$(echo "$_lang" | tr '[:lower:]' '[:upper:]')"
            echo "--build-arg DEV_${_upper}=false"
        done
        return
    fi
    for _item in "${_items[@]}"; do
        _item="$(echo "$_item" | tr -d '[:space:]')"
        [ "$_item" = "" ] && continue
        local _lower; _lower="$(echo "$_item" | tr '[:upper:]' '[:lower:]')"
        if [ "$_lower" = "none" ] || [ "$_lower" = "all" ]; then
            echo "Error: '${_item}' cannot be combined with other languages" >&2; exit 1
        fi
        local _canon; _canon="$(_resolve_lang "$_item")"
        if [ "$_canon" = "" ]; then
            echo "Error: unknown language '${_item}'. Valid: java, php, go, rust, cpp, python, javascript, dotnet (or 'all'/'none')" >&2; exit 1
        fi
        _enabled="${_enabled}${_canon} "
    done
    for _lang in "${_ALL_LANGS[@]}"; do
        if [[ "$_enabled" != *" ${_lang} "* ]]; then
            local _upper; _upper="$(echo "$_lang" | tr '[:lower:]' '[:upper:]')"
            echo "--build-arg DEV_${_upper}=false"
        fi
    done
}

# ── Validate a package list against a regex ──────────────────────
_validate_pkg_list() {
    local _val="$1" _regex="$2" _label="$3"
    local _pkgs _pkg
    read -ra _pkgs <<< "${_val//,/ }"
    for _pkg in "${_pkgs[@]}"; do
        [ "$_pkg" = "" ] && continue
        if ! [[ "$_pkg" =~ $_regex ]]; then
            echo "Warning: rejecting invalid package name '${_pkg}' in ${_label}" >&2
            return 1
        fi
    done
    return 0
}

# ── Format status line ────────────────────────────────────────────
# Args: engine dind_label dircache_count memory cpus [suffix]
# suffix is optional (e.g. "Container: new | Initializing..." or empty)
_format_status_line() {
    local _engine="$1" _dind="$2" _dircache="${3:-0}" _mem="$4" _cpus="$5" _suffix="${6:-}"
    local _line="Engine: ${_engine} | DinD: ${_dind}"
    [ "$_dircache" -gt 0 ] 2>/dev/null && _line+=" | CacheDirs: ${_dircache}"
    _line+=" | Memory: ${_mem} | CPUs: ${_cpus}"
    [ "$_suffix" != "" ] && _line+=" | ${_suffix}"
    echo "$_line"
}

# ── Test if a command basename is interactive ─────────────────────
# Returns 0 if $1 is in _INTERACTIVE_COMMANDS, 1 otherwise.
_is_interactive_command() {
    local _cmd="$1" _c
    for _c in "${_INTERACTIVE_COMMANDS[@]}"; do
        [ "$_cmd" = "$_c" ] && return 0
    done
    return 1
}

# ── Print to stderr unless in quiet mode (_QUIET=1) ───────────────
# shellcheck disable=SC2015  # || true is a set -e guard, not an else branch; echo rarely fails
_info() { [ "${_QUIET:-0}" = "0" ] && echo "$@" >&2 || true; }

# ── Print warning to stderr (always shown, bypasses quiet mode) ───
_warn() { echo "Warning: $*" >&2; }

