#!/bin/bash
# ==============================================================================================
# ============================= Docker Prune Images ============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Removes orphaned Docker images. Two modes:
#
# Default — dangling only (safe, fast):
#   Removes untagged images (no name, no container reference). These accumulate
#   after container updates pull a new image, leaving the old one untagged.
#   Running containers are never affected.
#
# --all — full orphan cleanup:
#   Step 1: removes stopped containers (exited/created state).
#   Step 2: removes all images not used by any running container.
#   Use this to clear tagged images left behind by removed or stopped apps.
#   CAUTION: also removes intentionally stopped containers — only run when you
#   know all stopped containers are safe to delete.
#
# ==============================================================================================
# DESIGN PRINCIPLES
# ==============================================================================================
#
# Safe Default, Explicit Escalation
#   The default mode (dangling only) is always safe — running containers are
#   never affected. The --all mode requires deliberate opt-in and carries an
#   explicit caution in the description, because it removes stopped containers
#   that may be intentionally paused.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# Default (dangling only):
#   docker image prune -f — removes untagged, unreferenced images only. Every tagged
#   image survives, running containers are untouched, and nothing stopped is removed.
#
# --all (full orphan cleanup):
#   1. Remove containers in exited/created state
#   2. Remove every image not used by a RUNNING container
#   Step 1 is what makes step 2 reach further: with the stopped containers gone, their
#   images are no longer referenced and become eligible. That is also precisely why
#   --all is destructive to anything deliberately kept stopped.
#
# Reclaimed space is reported for both modes.
#
# ==============================================================================================
# CONFIGURATION
# ==============================================================================================
#
# This tool is deliberately unconfigured — it reads live Docker state rather than any
# configured list, so there is nothing host-specific to alias and no thresholds to tune.
# Scope is controlled entirely by the mode flag (default vs --all).
#
# Note it does not call detect_hosts(): nothing here is host-specific, and it acts only on
# the local daemon.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Root Enforcement
#   Docker prune operations require root.
#
# Lock Acquisition
#   acquire_lock prevents concurrent prune runs racing on the same image store.
#
# Dangling-Only Default
#   The default mode removes untagged, unreferenced images only. Reaching anything
#   tagged, running, or deliberately stopped requires --all explicitly — the safe
#   behaviour is what you get by not thinking about it.
#
# Running Containers Never Touched
#   Neither mode removes a running container or an image a running container uses.
#   --all widens the blast radius to STOPPED containers and their images, never to
#   anything currently up.
#
# Dry Run Support
#   --dry-run lists everything that would be removed and removes nothing. Worth using
#   before --all specifically, since that mode deletes intentionally-stopped containers.
#
# Status Mode
#   --status lists current dangling images and stopped containers without changing
#   anything, so the scope of a prospective --all is visible up front.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# docker_prune_images.sh
#     Remove dangling (untagged) images only.
#
# docker_prune_images.sh --all
#     Remove stopped containers, then remove all unused images.
#
# docker_prune_images.sh --dry-run
#     Show what would be removed without making changes.
#
# docker_prune_images.sh --status
#     Show dangling images and stopped containers. No changes.
#
# ==============================================================================================

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

source "$SCRIPT_DIR/../load_config.sh"

# ── Handle --all before parse_args ────────────────────────────────────────────
ALL_MODE=false
FILTERED_ARGS=()
for _arg in "$@"; do
    [[ "$_arg" == "--all" ]] && ALL_MODE=true || FILTERED_ARGS+=("$_arg")
done
parse_args "${FILTERED_ARGS[@]}"

# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
    error "Must be run as root"
    exit 1
fi

acquire_lock

# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
    echo ""
    echo "━━━━━ $ICON_SUMMARY DOCKER PRUNE IMAGES STATUS ━━━━━"

    DANGLING=$(docker images -f "dangling=true" --format "{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedSince}}" 2>/dev/null)
    STOPPED=$(docker ps -a --filter "status=exited" --filter "status=created" \
        --format "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}" 2>/dev/null)

    echo ""
    echo "$ICON_CONTAINERS  Dangling images (no tag, no container):"
    if [[ -z "$DANGLING" ]]; then
        echo "    none"
    else
        echo "$DANGLING" | while IFS=$'\t' read -r id repo size age; do
            echo "    $id   $repo   $size   $age"
        done
    fi

    echo ""
    echo "$ICON_CONTAINERS  Stopped containers (--all would remove these first):"
    if [[ -z "$STOPPED" ]]; then
        echo "    none"
    else
        echo "$STOPPED" | while IFS=$'\t' read -r id name image status; do
            echo "    $name   ($image)   $status"
        done
    fi

    echo ""
    echo "━━━━━━━━━━━━━━━━━━━━━━━"
    exit 0
fi

# ==============================================================================================
# ━━━ Run ━━━
# ==============================================================================================
MODE_LABEL=$([[ "$ALL_MODE" == true ]] && echo "full orphan cleanup" || echo "dangling only")
log "$ICON_GEAR Config: mode=${MODE_LABEL} dry-run=${DRY_RUN}"
echo "━━━ $ICON_CONTAINERS Docker Prune Images ($MODE_LABEL) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"

TOTAL_RECLAIMED=0

# ── Step 1 (--all only): remove stopped containers ────────────────────────────
if [[ "$ALL_MODE" == true ]]; then
    STOPPED_IDS=$(docker ps -a --filter "status=exited" --filter "status=created" -q 2>/dev/null)
    STOPPED_COUNT=$(echo "$STOPPED_IDS" | grep -c . || echo 0)

    if [[ "$STOPPED_COUNT" -eq 0 ]]; then
        echo "No stopped containers"
    elif [[ "$DRY_RUN" == true ]]; then
        warn "DRY RUN — would remove $STOPPED_COUNT stopped container(s):"
        docker ps -a --filter "status=exited" --filter "status=created" \
            --format "  {{.Names}}   {{.Image}}   {{.Status}}" 2>/dev/null
    else
        echo "Removing $STOPPED_COUNT stopped container(s)..."
        docker container prune -f 2>&1 | grep -v "^Total\|^$" || true
        success "Removed $STOPPED_COUNT stopped container(s)"
    fi
    echo ""
fi

# ── Step 2: prune images ───────────────────────────────────────────────────────
PRUNE_FLAGS=$([[ "$ALL_MODE" == true ]] && echo "-a" || echo "")
PRUNE_FILTER=$([[ "$ALL_MODE" == true ]] && echo "" || echo '-f "dangling=true"')

# Preview count
if [[ "$ALL_MODE" == true ]]; then
    # Images not used by any running container
    RUNNING_IMAGES=$(docker ps --format "{{.Image}}" 2>/dev/null)
    UNUSED_COUNT=$(docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null \
        | grep -vxF "$RUNNING_IMAGES" | grep -c . || echo 0)
    TARGET_LABEL="$UNUSED_COUNT unused image(s)"
else
    DANGLING_IDS=$(docker images -f "dangling=true" -q 2>/dev/null)
    UNUSED_COUNT=$(echo "$DANGLING_IDS" | grep -c . || echo 0)
    TARGET_LABEL="$UNUSED_COUNT dangling image(s)"
fi

if [[ "$UNUSED_COUNT" -eq 0 ]]; then
    success "No images to remove"
    exit 0
fi

if [[ "$DRY_RUN" == true ]]; then
    warn "DRY RUN — would remove $TARGET_LABEL:"
    if [[ "$ALL_MODE" == true ]]; then
        docker images --format "  {{.Repository}}:{{.Tag}}   {{.Size}}   {{.CreatedSince}}" 2>/dev/null \
            | grep -vF "$(docker ps --format '{{.Image}}' 2>/dev/null)" || true
    else
        docker images -f "dangling=true" \
            --format "  {{.ID}}   {{.Size}}   created {{.CreatedSince}}" 2>/dev/null
    fi
    exit 0
fi

log "Pruning $TARGET_LABEL..."
OUTPUT=$(docker image prune $PRUNE_FLAGS -f 2>&1)
[[ "$ENABLE_LOGGING" == "true" ]] && echo "$OUTPUT" | sed 's/^/  /'
RECLAIMED=$(echo "$OUTPUT" | grep -E "^Total reclaimed" || echo "Total reclaimed space: unknown")
success "Done — $RECLAIMED"
