# Copyright (c) 2026 Progralink. Licensed under the DevCage License — see LICENSE.
# shellcheck shell=bash
#
# build.sh — Image fingerprinting and build-lock functions for the DevCage bash launcher.
# Sourced by devcage.sh. Depends on: SCRIPT_DIR, VERSION_BUILD_ARGS, CONTAINER_ENGINE,
# _FP_CACHE_FILE, _IMAGE_NAME, _BUILD_LOCK_FILE, _ORIGINAL_TMPDIR.

# ── Build lock state ──────────────────────────────────────────────
# Initialized here; orchestrator sets _IMAGE_NAME and _BUILD_LOCK_FILE before sourcing
# or immediately after (they depend on DEVCAGE_IMAGE which is resolved at runtime).
_BUILD_LOCK_ACQUIRED=0
_BUILD_LOCK_FD=""
_BUILD_LOCK_DIR=""

# ── Fingerprint helpers ───────────────────────────────────────────

_fp_mtime_key() {
    # Stat individual wrapper files (not just the directory) so edits are detected.
    # Glob expansion of bin/* and lib/* captures adds/removes too (changes the list).
    local _mtimes
    if stat --version >/dev/null 2>&1; then
        _mtimes="$(stat -c %Y "$SCRIPT_DIR/sandbox/image/Dockerfile" \
            "$SCRIPT_DIR/sandbox/docker-compose.yml" \
            "$SCRIPT_DIR/sandbox/image/wrappers/lib/"* \
            "$SCRIPT_DIR/sandbox/image/wrappers/bin/"* 2>/dev/null | tr '\n' ',')"
    else
        _mtimes="$(stat -f %m "$SCRIPT_DIR/sandbox/image/Dockerfile" \
            "$SCRIPT_DIR/sandbox/docker-compose.yml" \
            "$SCRIPT_DIR/sandbox/image/wrappers/lib/"* \
            "$SCRIPT_DIR/sandbox/image/wrappers/bin/"* 2>/dev/null | tr '\n' ',')"
    fi
    printf '%s%s' "$_mtimes" "$(printf '%s' "${VERSION_BUILD_ARGS[@]+"${VERSION_BUILD_ARGS[@]}"}")"
}
_build_fingerprint() {
    # Fast path: use cached fingerprint if build input mtimes haven't changed
    if [ -f "$_FP_CACHE_FILE" ]; then
        local _cached_mtime _cached_fp
        # Cache format: line 1 = mtime key, line 2 = fingerprint hash
        { read -r _cached_mtime; read -r _cached_fp; } < "$_FP_CACHE_FILE" 2>/dev/null
        if [ "${_cached_fp:-}" != "" ] && [ "${_cached_mtime:-}" != "" ]; then
            if [ "$(_fp_mtime_key)" = "$_cached_mtime" ]; then
                echo "$_cached_fp"
                return 0
            fi
        fi
    fi
    # Slow path: compute full fingerprint from file contents
    local _hash_input
    _hash_input="$(cat "$SCRIPT_DIR/sandbox/image/Dockerfile" 2>/dev/null)
$(find "$SCRIPT_DIR/sandbox/image/wrappers" -type f -print0 2>/dev/null | sort -z | xargs -0 cat 2>/dev/null)
$(printf '%s\n' "${VERSION_BUILD_ARGS[@]+"${VERSION_BUILD_ARGS[@]}"}")
$(cat "$SCRIPT_DIR/sandbox/docker-compose.yml" 2>/dev/null)"
    local _fp
    if command -v sha256sum >/dev/null 2>&1; then
        _fp="$(printf '%s' "$_hash_input" | sha256sum | cut -d' ' -f1)"
    elif command -v shasum >/dev/null 2>&1; then
        _fp="$(printf '%s' "$_hash_input" | shasum -a 256 | cut -d' ' -f1)"
    else
        _fp="no-hash"
    fi
    # Cache for next run (mtime key on line 1, hash on line 2)
    if [ "$_fp" != "no-hash" ]; then
        printf '%s\n%s\n' "$(_fp_mtime_key)" "$_fp" > "$_FP_CACHE_FILE" 2>/dev/null || true
    fi
    echo "$_fp"
}

# ── Build lock functions ──────────────────────────────────────────
# Prevent concurrent builds of the same image from stomping each other.
# Lock is per image name so different workspaces sharing an image serialize.

_acquire_build_lock() {
    if [ "$_BUILD_LOCK_ACQUIRED" = "1" ]; then return 0; fi
    # Try flock first (Linux, some macOS Homebrew)
    if command -v flock >/dev/null 2>&1; then
        exec 9>"$_BUILD_LOCK_FILE"
        local _waited=0
        while ! flock -n 9 2>/dev/null; do
            if [ "$_waited" = "0" ]; then
                echo "Waiting for another DevCage build to finish…" >&2
                _waited=1
            fi
            sleep 2
        done
        _BUILD_LOCK_FD=9
        _BUILD_LOCK_ACQUIRED=1
        return 0
    fi
    # Fallback: mkdir-based lock (atomic on all POSIX + Git Bash/Windows)
    local _lock_dir="${_BUILD_LOCK_FILE}.d"
    local _waited=0
    while true; do
        if mkdir "$_lock_dir" 2>/dev/null; then
            echo "$$" > "$_lock_dir/pid" 2>/dev/null || true
            _BUILD_LOCK_DIR="$_lock_dir"
            _BUILD_LOCK_ACQUIRED=1
            return 0
        fi
        # Check for stale lock: PID no longer running or lock older than 30 min
        local _lock_pid
        _lock_pid="$(cat "$_lock_dir/pid" 2>/dev/null || echo "")"
        if [ "$_lock_pid" != "" ] && ! kill -0 "$_lock_pid" 2>/dev/null; then
            rm -rf "$_lock_dir" 2>/dev/null || true
            continue
        fi
        # Age-based stale detection (PID check unreliable in Git Bash)
        if [ -f "$_lock_dir/pid" ]; then
            local _lock_age
            if stat --version >/dev/null 2>&1; then
                # GNU stat
                _lock_age=$(( $(date +%s) - $(stat -c %Y "$_lock_dir/pid" 2>/dev/null || echo "0") ))
            else
                # BSD stat (macOS)
                _lock_age=$(( $(date +%s) - $(stat -f %m "$_lock_dir/pid" 2>/dev/null || echo "0") ))
            fi
            if [ "$_lock_age" -gt 1800 ] 2>/dev/null; then
                rm -rf "$_lock_dir" 2>/dev/null || true
                continue
            fi
        fi
        if [ "$_waited" = "0" ]; then
            echo "Waiting for another DevCage build to finish…" >&2
            _waited=1
        fi
        sleep 2
    done
}

_release_build_lock() {
    if [ "$_BUILD_LOCK_ACQUIRED" != "1" ]; then return 0; fi
    if [ "$_BUILD_LOCK_FD" != "" ]; then
        # Validate FD is numeric before closing (avoid eval hazard)
        if [[ "$_BUILD_LOCK_FD" =~ ^[0-9]+$ ]]; then
            exec 9>&- 2>/dev/null || true
        fi
        rm -f "$_BUILD_LOCK_FILE" 2>/dev/null || true
        _BUILD_LOCK_FD=""
    fi
    if [ "$_BUILD_LOCK_DIR" != "" ]; then
        rm -rf "$_BUILD_LOCK_DIR" 2>/dev/null || true
        _BUILD_LOCK_DIR=""
    fi
    _BUILD_LOCK_ACQUIRED=0
}

# ── Image label inspection ────────────────────────────────────────
# Read both labels from a container image in a single inspect call (avoids
# multiple docker round-trips through WSL2, saving 2-4 seconds on Windows).
_image_labels() {
    "$CONTAINER_ENGINE" image inspect \
        --format '{{index .Config.Labels "dev-sandbox.version"}}|{{index .Config.Labels "dev-sandbox.fingerprint"}}' \
        "$_IMAGE_NAME" 2>/dev/null || echo "|"
}

# ── Build decision ────────────────────────────────────────────────
# Checks image labels against current version + fingerprint.
# If a build is needed: acquires lock, re-checks after lock (skips if another
# builder finished while waiting), builds the image, releases lock.
# Then calls _precreate_dircache_dirs() (from volumes.sh) to pre-create
# dircache subdirs in the cache volume (requires the image to exist first).
#
# Reads:  REBUILD, SKIP_BUILD, DEVCAGE_VERSION, DEVCAGE_IMAGE, CONTAINER_ENGINE,
#         VERSION_BUILD_ARGS, COMPOSE_FILES, BUILD_PROGRESS_ARGS
# Writes: SKIP_BUILD (may flip 0→1 or 1→0)
_make_build_decision() {
    local _LABELS _CURRENT_FP _LABEL_VER _LABEL_FP
    if [ "$REBUILD" != "1" ]; then
        _LABELS="$(_image_labels)"
        if [ "$_LABELS" != "|" ]; then
            _CURRENT_FP="$(_build_fingerprint)"
            _LABEL_VER="${_LABELS%%|*}"
            _LABEL_FP="${_LABELS#*|}"
            if [ "$_LABEL_FP" != "" ] && [ "$_LABEL_FP" != "unknown" ]; then
                if [ "$_LABEL_VER" = "$DEVCAGE_VERSION" ] && [ "$_LABEL_FP" = "$_CURRENT_FP" ]; then
                    # Fast path: image matches, no build needed
                    if [ "$SKIP_BUILD" = "0" ]; then
                        SKIP_BUILD=1
                    fi
                else
                    if [ "$_LABEL_VER" != "$DEVCAGE_VERSION" ]; then
                        echo "DevCage version changed (${_LABEL_VER} → ${DEVCAGE_VERSION}): rebuilding image…" >&2
                    else
                        echo "Build inputs changed: rebuilding image…" >&2
                    fi
                    SKIP_BUILD=0
                fi
            else
                # Image has no labels — must rebuild to add them
                echo "Image missing fingerprint labels: rebuilding image…" >&2
                SKIP_BUILD=0
            fi
        else
            # Image missing — must build regardless of --skip-build
            if [ "$SKIP_BUILD" = "1" ]; then
                echo "Warning: image '${_IMAGE_NAME}' not found, ignoring --skip-build." >&2
            fi
            SKIP_BUILD=0
        fi
    fi

    if [ "$SKIP_BUILD" = "1" ] && [ "$REBUILD" = "1" ]; then
        echo "Warning: --skip-build and --rebuild both set; skipping build." >&2
    fi

    if [ "$SKIP_BUILD" = "0" ]; then
        _acquire_build_lock
        # Re-check image labels after acquiring lock — another builder may have finished
        local _RECHECK_LABELS _RECHECK_VER _RECHECK_FP
        _RECHECK_LABELS="$(_image_labels)"
        if [ "$REBUILD" != "1" ] && [ "$_RECHECK_LABELS" != "|" ]; then
            _RECHECK_VER="${_RECHECK_LABELS%%|*}"
            _RECHECK_FP="${_RECHECK_LABELS#*|}"
            _CURRENT_FP="${_CURRENT_FP:-$(_build_fingerprint)}"
            if [ "$_RECHECK_FP" != "" ] && [ "$_RECHECK_FP" != "unknown" ] \
               && [ "$_RECHECK_VER" = "$DEVCAGE_VERSION" ] && [ "$_RECHECK_FP" = "$_CURRENT_FP" ]; then
                echo "Image was built by another instance while waiting; skipping build." >&2
                SKIP_BUILD=1
                _release_build_lock
            fi
        fi
    fi

    if [ "$SKIP_BUILD" = "0" ]; then
        local _BUILD_ARGS=()
        local _FP_BUILD_ARGS=(--build-arg "DEVCAGE_VERSION=${DEVCAGE_VERSION}" --build-arg "DEVCAGE_FINGERPRINT=$(_build_fingerprint)")
        if [ "$REBUILD" = "1" ]; then
            _BUILD_ARGS+=(--no-cache)
            echo "Rebuilding image from scratch (may take several minutes)…" >&2
        else
            echo "Building sandbox image (first run may take several minutes)…" >&2
        fi
        # Remove existing image tag so BuildKit can re-create it without conflict.
        "$CONTAINER_ENGINE" image rm "${DEVCAGE_IMAGE:-devcage-workspace}" >/dev/null 2>&1 || true
        "$CONTAINER_ENGINE" compose "${BUILD_PROGRESS_ARGS[@]+"${BUILD_PROGRESS_ARGS[@]}"}" "${COMPOSE_FILES[@]}" build --quiet "${_BUILD_ARGS[@]+"${_BUILD_ARGS[@]}"}" "${VERSION_BUILD_ARGS[@]+"${VERSION_BUILD_ARGS[@]}"}" "${_FP_BUILD_ARGS[@]}" || \
            "$CONTAINER_ENGINE" compose "${BUILD_PROGRESS_ARGS[@]+"${BUILD_PROGRESS_ARGS[@]}"}" "${COMPOSE_FILES[@]}" build "${_BUILD_ARGS[@]+"${_BUILD_ARGS[@]}"}" "${VERSION_BUILD_ARGS[@]+"${VERSION_BUILD_ARGS[@]}"}" "${_FP_BUILD_ARGS[@]}"
        _release_build_lock
        # Prune dangling images left by rebuild
        if [ "$REBUILD" = "1" ]; then
            "$CONTAINER_ENGINE" image prune -f >/dev/null 2>&1 || true
        fi
    fi

    # Pre-create dircache subdirs in cache volume (requires image to exist).
    _precreate_dircache_dirs
}
