#!/bin/bash
# This script can be used to start, stop, create, and delete containers.
# It is what confd use, with the Finit container@.conf template, to set
# up, run, and delete containers.
#
# NOTE: when creating/deleting containers, remember 'initctl reload' to
#       activate the changes!  In confd this is already handled.
#
# TODO: this script should be refactored to take a .cfg file instead for
#       each container.  For details, see this pull request discussion:
#       https://github.com/kernelkit/infix/pull/1151#discussion_r2444851292
#
# shellcheck disable=SC1001
CLEANUP=/var/lib/containers/cleanup
DOWNLOADS=/var/lib/containers/oci
BUILTIN=/lib/oci
BASEDIR=/var/tmp
container=$0
checksum=""
extracted=
timeout=30		    # NOTE: matched with container@.conf
dir=""
all=""
env=""
port=""
force=${force:-}
reset_volumes=

# Variable shared across subshells
export meta_sha=""

dbg()
{
    logger -I $PPID -t container -p local1.debug -- "$*"
}

log()
{
    logger -I $PPID -t container -p local1.notice -- "$*"
}

err()
{
    rc=$1; shift
    logger -I $PPID -t container -p local1.err -- "Error: $*"

    if [ -n "$extracted" ] && [ -n "$tmpdir" ]; then
	if [ -d "$tmpdir" ]; then
	    log "Cleaning up temporary directory $tmpdir"
	    rm -rf "$tmpdir"
	fi
    fi

    [ "$rc" -eq 0 ] || exit "$rc"
}

pidfn()
{
    echo "/run/containers/${1}.pid"
}

calc_sha()
{
    sha256sum "$1" 2>/dev/null | awk '{print $1}'
}

# Calculate a combined SHA256 over the container script and its
# optional env file.  Environment variables are stored separately
# from the script, so both must be included in the checksum to
# detect configuration changes such as added/changed env vars.
calc_config_sha()
{
    _envfile="/run/containers/args/${name}.env"

    if [ -f "$_envfile" ]; then
	cat "$1" "$_envfile" | sha256sum | awk '{print $1}'
    else
	calc_sha "$1"
    fi
}

# Check image transport, return 0 if remote, 1 if local
is_remote()
{
    image=$(awk '/^# meta-image:/ {print $3}' "$1" 2>/dev/null || echo "")
    echo "$image"

    case "$image" in
	oci\:* | oci-archive\:* | docker-archive\:* | docker-daemon\:* | \
	    dir\:* | containers-storage\:* | ostree\:* | sif\:* | /*)
	    return 1
	    ;;
	*)
	    return 0
	    ;;
    esac
}

# Check loaded image in container store vs archive, as well as verifying
# the container instance vs. the configuration.
#
# This is an optimization to cut down startup times.
is_uptodate()
{
    img_sha=$(awk '/^# meta-image-sha256:/ {print $3}' "$script" 2>/dev/null)
    if [ -n "$img_sha" ]; then
	# Local image optimization: check if archive SHA matches stored sidecar file
	img=$(awk '/^# meta-image:/ {print $3}' "$script" 2>/dev/null)
	if [ -n "$img" ]; then
	    case "$img" in
		docker-archive\:*)
		    archive_path="${img#docker-archive:}"
		    ;;
		oci-archive\:*)
		    archive_path="${img#oci-archive:}"
		    ;;
		*)
		    archive_path="$img"
		    ;;
	    esac

 	    # Handle relative paths - check BUILTIN and DOWNLOADS
	    if [ ! -f "$archive_path" ]; then
		if [ -f "$DOWNLOADS/$(basename "$archive_path")" ]; then
		    archive_path="$DOWNLOADS/$(basename "$archive_path")"
		elif [ -f "$BUILTIN/$(basename "$archive_path")" ]; then
		    archive_path="$BUILTIN/$(basename "$archive_path")"
		fi
	    fi

	    # Check if the archive exists and compare SHA with sidecar file
	    if [ -f "$archive_path" ]; then
		archive_basename=$(basename "$archive_path")
		sha_file="$DOWNLOADS/${archive_basename}.sha256"

		if [ -f "$sha_file" ]; then
		    stored_sha=$(cat "$sha_file" 2>/dev/null)
		    current_sha=$(calc_sha "$archive_path")

		    # If SHA matches, check container instance
		    if [ "$stored_sha" = "$current_sha" ]; then
			if podman container exists "$name"; then
			    config_sha=$(calc_config_sha "$script")
			    container_sha=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null)
			    container_img_sha=$(podman inspect "$name" --format '{{index .Config.Labels "meta-image-sha256"}}' 2>/dev/null)

			    if [ "$container_img_sha" = "$img_sha" ] && [ "$container_sha" = "$config_sha" ]; then
				# Unmodified local image and configuration
				return 0
			    fi
			fi
		    else
			# Archive changed (e.g., rootfs upgrade) - need to reload
			dbg "Archive SHA changed, will reload image and recreate container"
		    fi
		fi
	    fi
	fi
    else
	# Remote image optimization: check config-sha256 only
	if podman container exists "$name"; then
	    config_sha=$(calc_config_sha "$script")
	    container_sha=$(podman inspect "$name" --format '{{index .Config.Labels "config-sha256"}}' 2>/dev/null)

	    if [ "$container_sha" = "$config_sha" ]; then
		# Unmodified local image and configuration
		return 0
	    fi
	fi
    fi

    return 1
}

check()
{
    file=$1

    if [ -z "$checksum" ]; then
	log "no checksum to verify $file against, continuing."
	return 0
    fi

    if echo "${checksum}  ${file}" | "$cmdsum" -c -s; then
        log "$file checksum verified OK."
	return 0
    fi

    got=$("$cmdsum" "${file}" | awk '{print $1}')
    log "$file checksum mismatch, got $got, expected $checksum, removing file."
    rm -f "$file"

    return 1
}

# Fetch an OCI image over ftp/http/https.  Use wget for FTP, which curl
# empirically does not work well with.  Log progress+ & error to syslog.
fetch()
{
    url=$1
    file=$(basename "$url")
    dst="$DOWNLOADS/$file"

    cd "$DOWNLOADS" || return
    if [ -f "$file" ]; then
	if [ -n "$force" ]; then
	    log "Force flag set, removing cached $file and re-downloading."
	    rm -f "$file"
	else
	    log "$file already available."
	    if check "$file"; then
		echo "$dst"
		return 0
	    fi
	fi
    fi

    log "Fetching $url"

    if echo "$url" | grep -qE "^ftp://"; then
        cmd="wget -q $url"
    elif echo "$url" | grep -qE "^https?://"; then
        cmd="curl $creds -sSL --fail -o \"$file\" $url"
    else
        log "Unsupported URL scheme: $url"
        return 1
    fi

    if out=$(eval "$cmd" 2>&1); then
	log "$file downloaded successfully."
	if [ -n "$force" ] || check "$file"; then
	    echo "$dst"
	    return 0
	fi
    fi

    # log error message from backend
    while IFS= read -r line; do
	log "$line"
    done <<EOF
$out
EOF
    return 1
}

# Extracts an oci-archive.tar[.gz] in a temporary directory.  Finds and
# sanity checks that at least one index.json exist in the archive. This
# is the OCI directory fed to `podman load` and also used as repo name.
# NOTE: if there are >1 index.json, this function does not handle them.
load_archive()
{
    uri=$1
    tag=$2
    tmp=$3
    img=$(basename "$uri")

    # Supported transports for load and create
    case "$uri" in
	oci:*)			# Unpacked OCI image
	    file=${uri#oci:}
	    ;;
	oci-archive:*)		# Packed OCI image, .tar or .tar.gz format
	    file=${uri#oci-archive:}
	    ;;
	ftp://* | http://* | https://*)
            if ! file=$(fetch "$uri"); then
		return 1
	    fi
            ;;
	*)			# docker://*, docker-archive:*, or URL
	    # Skip existence check if force flag is set (e.g., for upgrade command)
	    if [ -z "$force" ] && podman image exists "$img"; then
		echo "$img"
		return 0
	    fi
	    # XXX: use --retry=0 with Podman 5.0 or later.
	    if ! id=$(podman pull --quiet "$uri"); then
		log "Failed pulling $uri"
		return 1
	    fi
	    # Echo image tag to caller
	    podman images --filter id="$id" --format "{{.Repository}}:{{.Tag}}"
	    return 0
	    ;;
    esac

    if [ ! -f "$file" ]; then
	if [ -f "$DOWNLOADS/$file" ]; then
	    file="$DOWNLOADS/$file"
	elif [ -f "$BUILTIN/$file" ]; then
	    file="$BUILTIN/$file"
	else
	    err 1 "cannot find OCI archive $file in URI $uri"
	fi
    fi

    file=$(realpath "$file")
    if [ -d "$file" ]; then
	index=$(find "$file" -name index.json)
	if [ -z "$index" ]; then
	    err 1 "cannot find index.json in OCI image $file"
	fi
    else
	# Extract files in a temporary directory, because most OCI
	# archives are flat/bare, all files in the root w/o a dir/
	tmpdir=$(mktemp -d -p "$BASEDIR") || err 1 "failed creating temporary directory"
	cd "$tmpdir" || err 1 "failed cd to temporary directory $tmpdir"

	index="$tmpdir/$(tar tf "$file" |grep index.json)"
	if [ -z "$index" ]; then
	    err 1 "invalid OCI archive, cannot find index.json in $file"
	fi

	[ -n "$quiet" ] || log "Extracting OCI archive $file ..."
        tar xf "$file"  || err 1 "failed unpacking $file in $tmpdir"
	extracted=true
	cd - >/dev/null || err 0 "failed cd -"
    fi

    dir=$(dirname "$index")

    # Handle flat tarballs without a sub-directory, because
    # the $dir name is used as fallback when retagging below.
    if [ -n "$extracted" ] && [ "$dir" = "$tmpdir" ]; then
	parent=$(dirname "$dir")
	dirnam=$(echo "$img" | sed 's/\(.*\)\.tar.*/\1/')
	tmpdir="${parent}/${dirnam}"
	mv "$dir" "$tmpdir"
	dir="$tmpdir"
    fi

    if basename "$dir" | grep -q ":"; then
        if [ -z "$tag" ]; then
            tag=$(basename "$dir")
        fi

        sanitized_dir=$(echo "$dir" | cut -d':' -f1)
        mv "$dir" "$sanitized_dir" || err 1 "failed renaming $dir to $sanitized_dir"
        dir="$sanitized_dir"
    fi

    [ -n "$quiet" ] || log "Loading OCI image $dir ..."
    output=$(nice podman load -qi "$dir")

    # Extract image ID from podman load output:
    # "Loaded image: sha256:cd9d0aaf81be..."
    if echo "$output" | grep -q "sha256:"; then
	img_id="${output##*sha256:}"
    else
	# Fallback to directory name if no SHA found
	img_id="$dir"
    fi

    # On podman < 4.7.0 we had to retag images from default $dir:latest
    # From >= 4.7.0 we always tag since loads come in as <none>:<none>
    if [ -z "$tag" ]; then
	tag=$(basename "$dir")
    fi

    # Repo names must be lowercase, and only '[a-z0-9._/-]+' and ':tag'
    tag=$(printf "%s" "$tag" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9._/:-' '-')

    [ -n "$quiet" ] || log "Tagging loaded image $img_id as $tag"
    if ! podman tag "$img_id" "$tag"; then
	err 1 "failed tagging image as $tag"
    fi

    # Save archive SHA256 to sidecar file to enable optimization
    # This applies to both local archives and downloaded remote archives
    if [ -f "$file" ]; then
	img_sha256=$(calc_sha "$file")
	archive_basename=$(basename "$file")
	sha_file="$DOWNLOADS/${archive_basename}.sha256"

	mkdir -p "$DOWNLOADS"

	echo "$img_sha256" > "$sha_file"
	if [ -n "$tmp" ]; then
	    echo "$img_sha256" > "$tmp"
	fi

	log "Saved archive checksum to $sha_file"
    fi

    # Clean up after ourselves
    if [ -n "$extracted" ]; then
	log "Cleaning up extracted $dir"
	rm -rf "$tmpdir"
    fi

    echo "$tag"
}

running()
{
    status=$(podman inspect -f '{{.State.Status}}' "$1" 2>/dev/null)
    [ "$status" = "running" ] && return 0
    return 1
}

# shellcheck disable=SC2086
create()
{
    name=$1
    image=$2
    shift 2

    if [ -z "$name" ] || [ -z "$image" ]; then
	echo "Usage:"
	echo "  container create NAME IMAGE"
	exit 1
    fi

    # Unpack and load docker-archive/oci/oci-archive, returning image
    # name, or return docker:// URL for download.
    sha_file=$(mktemp)
    if ! image=$(load_archive "$image" "" "$sha_file"); then
	exit 1
    fi

    if [ -s "$sha_file" ]; then
	img_sha=$(cat "$sha_file" 2>/dev/null || echo "")
	rm "$sha_file"
    fi

    if [ -z "$logging" ]; then
	logging="--log-driver syslog"
    fi

    # Build resource limit arguments
    resource=""
    if [ -n "$memory" ]; then
	resource="$resource --memory=$memory"
    fi
    if [ -n "$cpu_limit" ]; then
	resource="$resource --cpu-quota=$cpu_limit"
    fi

    # When we get here we've already fetched, or pulled, the image
    args="$args --read-only --replace --quiet $caps"
    args="$args --cgroups=enabled --cgroupns=host --cgroup-parent=system/container@$name"
    args="$args --restart=$restart --systemd=false --tz=local $privileged"
    args="$args $vol $mount $hostname $entrypoint $env $port $logging $resource"
    pidfile=/run/container:${name}.pid

    [ -n "$quiet" ] || log "---------------------------------------"
    [ -n "$quiet" ] || log "Got name: $name image: $image"
    [ -n "$quiet" ] || log "Got networks: $network"

    if [ -n "$network" ]; then
	for net in $network; do
	    args="$args --net=$net"
	done

	for srv in $dns; do
	    args="$args --dns=$srv"
	done

	for domain in $search; do
	    args="$args --dns-search=$domain"
	done
    else
	args="$args --network=none"
    fi

    # Add optimization labels for meta-image-sha256 and config checksum
    script="/run/containers/${name}.sh"
    if [ -f "$script" ]; then
 	if [ -z "$img_sha" ]; then
	    # Extract meta-image-sha256 from script if present
	    img_sha=$(awk '/^# meta-image-sha256:/ {print $3}' "$script" 2>/dev/null)
	fi
	if [ -n "$img_sha" ]; then
	    args="$args --label meta-image-sha256=$img_sha"
	fi

	# Add config checksum label
	args="$args --label config-sha256=$(calc_config_sha "$script")"
    fi

    # shellcheck disable=SC2048
    log "podman create --name $name --conmon-pidfile=$pidfile $args $image $*"
    if nice podman create --name "$name" --conmon-pidfile="$pidfile" $args "$image" $*; then
	[ -n "$quiet"  ] || log "Successfully created container $name from $image"
	[ -n "$manual" ] || start "$name"

	# Should already be enabled by confd (this is for manual use)
	initctl -bnq enable "container@${name}.conf"
	exit 0
    fi

    err 1 "failed creating container $name, please check the configuration."
}

delete()
{
    name=$1

    if [ -z "$name" ]; then
	echo "Usage:"
	echo "  container delete NAME"
	exit 1
    fi

    # Should already be stopped, but if not ...
    container stop "$name" >/dev/null

    while running "$name"; do
	log "$name: still running, waiting for it to stop ..."
	_=$((timeout -= 1))
	if [ $timeout -le 0 ]; then
	    err 1 "timed out waiting for container $1 to stop before deleting it."
	fi
	sleep 1
    done

    # NOTE: -v does not remove *named volumes*
    podman rm -vif "$name" >/dev/null 2>&1
    [ -n "$quiet" ] || log "Container $name has been removed."
}

# Called by Finit cleanup:script when a container service is removed.
# Processes all container instance IDs found in $CLEANUP/<name>/ directory
# and removes them. This handles the case where a container with the same
# name but different configuration replaces an old one.
#
# Note: Volumes are NOT automatically removed to prevent accidental data loss.
#       Use 'admin-exec container prune' to clean up unused volumes manually.
cleanup()
{
    if [ -z "$name" ]; then
	log "cleanup: missing container name"
	return 1
    fi

    cleanup_dir="$CLEANUP/$name"
    if [ ! -d "$cleanup_dir" ]; then
	log "cleanup: no cleanup directory for $name, nothing to do"
	return 0
    fi

    log "Cleaning up container instances for: $name"

    # Remove all container instances by ID from the cleanup directory
    for id_file in "$cleanup_dir"/*; do
	[ -f "$id_file" ] || continue
	cid=$(basename "$id_file")

	# Extract image name and meta-image-sha256 from the container instance labels
	# These are the image in container store and the trace to an oci-archive.tar.gz
	img=$(podman inspect "$cid" 2>/dev/null | jq -r '.[].ImageName' 2>/dev/null || echo "")
	sha=$(podman inspect "$cid" --format '{{index .Config.Labels "meta-image-sha256"}}' 2>/dev/null || echo "")

	log "Removing container instance with ID: ${cid:0:12}..."
	podman rm -vif "$cid" >/dev/null 2>&1
	rm -f "$id_file"

	# Clean up the image if it exists and is not used by other containers
	if [ -n "$img" ] && [ "$img" != "null" ]; then
	    if ! podman ps -a --format "{{.Image}}" | grep -q "^${img}$"; then
		log "Removing unused image: $img"
		podman rmi "$img" 2>/dev/null || true

		# Also remove archive and sidecar checksum file from $DOWNLOADS for remote images
		# I.e., $DOWNLOADS/oci-archive.tar.gz and $DOWNLOADS/oci-archive.tar.gz.sha256
		if [ -n "$sha" ] && [ -d "$DOWNLOADS" ]; then
		    # Search for matching sidecar file containing this SHA
		    for sha_file in "$DOWNLOADS"/*.sha256; do
			[ -f "$sha_file" ] || continue
			stored_sha=$(cat "$sha_file" 2>/dev/null || echo "")
			if [ "$stored_sha" = "$sha" ]; then
			    # Found matching sidecar - derive archive filename by stripping .sha256
			    archive="${sha_file%.sha256}"
			    if [ -f "$archive" ]; then
				# Safety check: verify archive SHA matches before removing
				if [ "$(calc_sha "$archive")" = "$sha" ]; then
				    log "Removing archive: $archive"
				    rm -f "$archive"
				else
				    log "Warning: archive SHA mismatch for $archive, not removing"
				fi
			    fi

			    log "Removing sidecar file: $sha_file"
			    rm -f "$sha_file"
			    break
			fi
		    done
		fi
	    else
		log "Image $img still in use by other containers, not removing"
	    fi
	fi
    done

    # Remove the cleanup directory for this container, and parent if empty
    rmdir "$cleanup_dir" 2>/dev/null
    rmdir "$CLEANUP" 2>/dev/null

    exit 0
}

waitfor()
{
    while [ ! -f "$1" ]; do
	_=$((timeout -= 1))
	if [ $timeout -le 0 ]; then
	    err 1 "timed out waiting for $1, aborting!"
	fi
	sleep 1;
    done
}

start()
{
    name=$1

    if running "$name"; then
	[ -n "$quiet" ] || echo "$name: already running."
	return
    fi

    initctl start container:$name
    # Real work is done by wrap() courtesy of finit sysv emulation
}

stop()
{
    name=$1

    if ! running "$name"; then
	[ -n "$quiet" ] || echo "$name: not running."
	return
    fi

    initctl stop container:$name
    # Real work is done by wrap() courtesy of finit sysv emulation
}

# When called by Finit: `initctl stop foo`, the container script
# is called as `container -n $foo stop`.  For the stop command
# we want to bypass the default 10 second timeout.
#
# NOTE: containers have three phases: setup, running, and teardown.
#       the setup phase is this script trying to fetch the image.
wrap()
{
    name=$1
    cmd=$2
    args=
    pidfile=$(pidfn "$name")

    if [ "$cmd" = "stop" ]; then
	# The setup phase may run forever in the background trying to fetch
	# the image.  It saves its PID in /run/containers/${name}.pid.  Kill
	# any in-flight setup, then fall through to podman stop -- a stale
	# pidfile is not proof the container isn't running.
	if [ -f "$pidfile" ]; then
            pid=$(cat "$pidfile")

	    # Check if setup is still running ...
	    if kill -0 "$pid" 2>/dev/null; then
		kill "$pid"
		wait "$pid" 2>/dev/null
            fi

            rm -f "$pidfile"
	fi

	# Only the 'podman stop' command takes -i (ignore missing) and --timeout
	args="-i --timeout $timeout"
    fi

    # Skip "echo $name" from podman start in log
    # shellcheck disable=SC2086
    podman "$cmd" $args "$name" >/dev/null
}

# Removes network $1 from all containers
netwrm()
{
    net=$1

    for c in $(podman ps $all --format "{{.Names}}"); do
	for n in $(podman inspect "$c" |jq -r '.[].NetworkSettings.Networks | keys[]'); do
	    if [ "$n" = "$net" ]; then
		podman network disconnect $force "$n" "$c" >/dev/null
	    fi
	done
    done
}

# Schedule restart of (any) container using network $1 to activate network changes
netrestart()
{
    net=$1

    for c in $(podman ps $all --format "{{.Names}}"); do
	for n in $(podman inspect "$c" |jq -r '.[].NetworkSettings.Networks | keys[]'); do
	    if [ "$n" = "$net" ]; then
		initctl -nbq touch "container@$c"
	    fi
	done
    done
}

atexit()
{
    pidfile=$(pidfn "$name")

    log "Received signal, exiting."
    if [ -n "$name" ] && [ -f "$pidfile" ]; then
	log "$name: in setup phase, removing $pidfile ..."
	rm -f "$pidfile"
    fi

    exit 1
}

usage()
{
	cat <<EOF
usage:
  container [opt] cmd [arg]

options:
  -a, --all                Show all, do all, remove all, of something
      --dns NAMESERVER     Set nameserver(s) when creating a container
      --dns-search LIST    Set host lookup search list when creating container
      --cap-add CAP        Add capability to unprivileged container
      --cap-drop CAP       Drop capability, for privileged containter
      --checksum TYPE:SUM  Use md5/sha256/sha512 to verify ftp/http/https archives
  -c, --creds USR[:PWD]    Credentials to pass to curl -u for remote ops
  -d, --detach             Detach a container started with 'run IMG [CMD]'
  -e, --env FILE           Environment variables when creating container
      --entrypoint         Disable container image's ENTRYPOINT, run cmd + arg
  -f, --force              Force operation, e.g. remove, or force image re-fetch
  -h, --help               Show this help text
      --hostname NAME      Set hostname when creating container
      --net NETWORK        Network interface(s) when creating or finding container
  -l, --log-driver DRV     Log driver to use
      --log-opt OPT        Logging options to log driver
      --log-path PATH      Path for k8s-file log pipe
  -m, --mount HOST:DEST    Bind mount a read-only file inside a container
      --manual             Do not start container automatically after creation
      --memory BYTES       Memory limit in bytes (supports K/M/G suffix)
      --cpu-limit LIMIT    CPU limit in millicores (1000m = 100% of 1 core)
  -n, --name NAME          Alternative way of supplying name to start/stop/restart
      --privileged         Give container extended privileges
  -p, --publish PORT       Publish ports when creating container
                           Syntax: [[ip:][hostPort]:]containerPort[/protocol]
  -q, --quiet              Quiet operation, called from confd
  -r, --restart POLICY     One of "no", "always", or "on-failure:NUM"
  -s, --simple             Show output in simplified format
  -t, --timeout SEC        Set timeout for delete/restart commands, default: 30
  -v, --volume NAME:PATH   Create named volume mounted inside container on PATH

commands:
  create   NAME IMAGE NET  Create container NAME using IMAGE with networks NET
  delete  [network] NAME   Remove container NAME or network NAME from all containers
  exec     NAME CMD        Run a command inside a container
  flush                    Clean up lingering containers and associated anonymous volumes
  find    [ifname PID]     Find PID of container where '--net IFNAME' currently lives
                           or, find the name of our IFNAME inside the container @PID
  help                     Show this help text
  list    [image | oci]    List names (only) of containers, images, or OCI archives
  load    [NAME | URL] NM  Load OCI tarball fileNAME or URL to image NM
  locate                   Find container that currently owns '--net IFNAME'
  remove  [IMAGE]          Remove an image, or prune unused images including OCI archives
  restart [network] NAME   Restart a (crashed) container or container(s) using network
  run      NAME [CMD]      Run a container interactively, with an optional command
  save     IMAGE FILE      Save a container image to an OCI tarball FILE[.tar.gz]
  setup    NAME            Create and set up container as a Finit task
  shell   [CMD]            Start a shell, or run CMD, inside a container
  show    [image | volume] Show containers, images, or volumes
  stat                     Show continuous stats about containers (Ctrl-C aborts)
  start   [NAME]           Start a container, see -n
  stop    [NAME]           Stop a container, see -n
  upgrade  NAME            Upgrade a running container (stop, pull, restart)
  volume  [prune]          Prune unused volumes
EOF
}

while [ "$1" != "" ]; do
    case $1 in
	-a | --all)
	    all="-a"
	    ;;
	--cap-add)
	    shift
	    caps="$caps --cap-add=$1"
	    ;;
	--cap-drop)
	    shift
	    caps="$caps --cap-drop=$1"
	    ;;
	--checksum)
	    shift
	    type="${1%%:*}"
	    checksum="${1#*:}"
	    case "$type" in
		md5)
		    cmdsum=md5sum
		    ;;
		sha256)
		    cmdsum=sha256sum
		    ;;
		sha512)
		    cmdsum=sha512sum
		    ;;
		*)
		    err 1 "Unsupported checksum type: $type"
		    ;;
	    esac
	    ;;
	-c | --creds)
	    shift
	    creds="-u $1"
	    ;;
	-d | --detach)
	    detach="-d"
	    ;;
	--dns)
	    shift
	    dns="$dns $1"
	    ;;
	--dns-search)
	    shift
	    search="$search $1"
	    ;;
	-e | --env)
	    shift
	    env="$env --env-file=$1"
	    ;;
	--entrypoint)
	    entrypoint="--entrypoint=\"\""
	    ;;
	-f | --force)
	    force="-f"
	    ;;
	-h | --help)
	    usage
	    exit 0
	    ;;
	--hostname)
	    shift
	    hostname="--hostname $1"
	    ;;
	-l | --log-driver)
	    shift
	    logging=" --log-driver=$1"
	    ;;
	--log-opt)
	    shift
	    logging="$logging --log-opt $1"
	    ;;
	--log-path)
	    shift
	    logging="$logging --log-opt path=$1"
	    ;;
	-m | --mount)
	    shift
	    mount="$mount --mount=$1"
	    ;;
	--manual)
	    manual=true
	    ;;
	--memory)
	    shift
	    memory="$1"
	    ;;
	--cpu-limit)
	    shift
	    cpu_limit="$1"
	    ;;
	-n | --name)
	    shift
	    name="$1"
	    ;;
	--net)
	    shift
	    if [ -n "$network" ]; then
		network="$network $1"
	    else
		network=$1
	    fi
	    ;;
	--privileged)
	    privileged="--privileged=true"
	    ;;
	-p | --publish)
	    shift
	    port="$port -p $1"
	    ;;
	-q | --quiet)
	    quiet="-q"
	    ;;
	-r | --restart)
	    shift
	    restart=$1
	    ;;
	-R | --reset-volumes)
	    reset_volumes=true
	    ;;
	-s | --simple)
	    simple=true
	    ;;
	-t | --timeout)
	    shift
	    timeout=$1
	    ;;
	-v | --volume)
	    shift
	    vol="$vol -v $1"
	    ;;
	*)
	    break
	    ;;
    esac
    shift
done

cmd=$1
if [ -n "$cmd" ]; then
    shift
fi

trap atexit INT HUP TERM

case $cmd in
    # Does not work atm., cannot attach to TTY because
    # we monitor 'podman start -ai foo' with Finit.
    # attach)
    # 	podman attach "$1"
    # 	;;
    cleanup) # Hidden from public view, use remove or flush commands instead
	cleanup "$@"
	;;
    create)
	[ -n "$quiet" ] || log "Got create args: $*"
	create "$@"
	;;
    delete)
	cmd=$1
	[ -n "$name" ] || name=$2
	if [ "$cmd" = "network" ] && [ -n "$name" ]; then
	    netwrm "$name"
	else
	    [ -n "$name" ] || name=$1
	    delete "$name"
	fi
	;;
    exec)
	if [ -z "$name" ]; then
	    name="$1"
	    shift
	fi
	podman exec -i "$name" "$@"
	;;
    flush)
	echo "Cleaning up any lingering containers";
	podman rm -av $force
	;;
    find)
	cmd=$1
	pid=$2
	if [ "$cmd" = "ifname" ] && [ -n "$pid" ]; then
	    nsenter -t "$pid" -n ip -d -j link | \
		jq --arg ifname "$network" -r '.[] | select(.ifalias==$ifname) | .ifname'
	else
	    containers=$(podman ps $all --format "{{.Names}}")
	    for c in $containers; do
		json=$(podman inspect "$c")
		nets=$(echo "$json" |jq -r '.[].NetworkSettings.Networks | keys[]' 2>/dev/null)
		for n in $nets; do
		    if [ "$network" = "$n" ]; then
			pid=$(echo "$json" | jq .[].State.Pid)
			echo "$pid"
			exit 0
		    fi
		done
	    done
	fi
	;;
    help)
	usage
	;;
    load)
	# shellcheck disable=SC2086
	name=$(load_archive "$1" $2)
	[ -n "$name" ] || exit 1

	# Show resulting image(s) matching $name
	podman images -n "$name"
	;;
    locate)			# Find where the host's ifname lives
	if [ -z "$network" ]; then
	    echo "Missing --net IFNAME option."
	    exit 1
	fi
	containers=$(podman ps $all --format "{{.Names}}")
	for c in $containers; do
	    json=$(podman inspect "$c")
	    nets=$(echo "$json" |jq -r '.[].NetworkSettings.Networks | keys[]' 2>/dev/null)
	    for n in $nets; do
		if [ "$network" = "$n" ]; then
		    echo "$c"
		    exit 0;
		fi
	    done
	done
	;;
    ls | list)
	cmd=$1
	[ -n "$cmd" ] && shift
	case $cmd in
	    image*)
		podman images $all --format "{{.Repository}}:{{.Tag}}"
		;;
	    oci)
		find $BUILTIN $DOWNLOADS -type f 2>/dev/null
		;;
	    *)
		podman ps $all --format "{{.Names}}"
		;;
	esac
	;;
    pull)
	podman pull "$@"
	;;
    remove)
	if [ -n "$all" ]; then
	    log "Removing all OCI archives from $DOWNLOADS directory ..."
	    find "${DOWNLOADS:?}" -mindepth 1 -exec rm -rf {} +
	    log "Removing all unused container images ..."
	    podman image prune $all $force
	else
	    podman rmi $force -i "$1"
	fi
	;;
    run)
	img=$1
	cmd=$2
	[ -n "$port" ] || port="-P"
	if [ -n "$cmd" ]; then
	    shift 2
	    [ -n "$detach" ] || echo "Starting $img ENTRYPOINT $cmd :: use Ctrl-p Ctrl-q to detach"
	    podman run -it --rm $detach $port --entrypoint="$cmd" "$img" "$@"
	else
	    [ -n "$detach" ] || echo "Starting $img :: use Ctrl-p Ctrl-q to detach"
	    podman run -it --rm $detach $port "$img"
	fi
	;;
    save)
	name=$1
	file=$2
	if echo "$file" | grep -q ".gz"; then
	    file=${file%%.gz}
	    gzip=true
	fi
	if ! echo "$file" | grep -q ".tar"; then
	    file=${file}.tar
	    gzip=true
	fi
	podman save -o "$file" "$name"
	if [ -s "$file" ] && [ -n "$gzip" ]; then
	    gzip "$file"
	fi
	;;
    setup)
	[ -n "$name" ] || err 1 "setup: missing container name."
	script=/run/containers/${name}.sh
	[ -x "$script" ] || err 1 "setup: $script does not exist or is not executable."

	if is_uptodate "$script"; then
	    log "Container $name config unchanged, skipping setup"
	    exit 0
	fi

	# Capture old image ID before recreation (for surgical cleanup after)
	old_image_id=""
	if podman container exists "$name"; then
	    old_image_id=$(podman inspect "$name" --format '{{.ImageID}}' 2>/dev/null)
	fi

	# Save our PID in case we get stuck here and someone wants to
	# stop us, e.g., due to reconfiguration or reboot.
	pidfile=$(pidfn "${name}")
	echo $$ > "$pidfile"

	if image=$(is_remote "$script"); then
	    while ! "$script"; do
		log "${name}: setup failed, waiting for network changes ..."

		# Timeout and retry after 60 seconds, on SIGTERM, or when
		# any network event is caught.
		timeout -s TERM -k 1 60 sh -c \
			'ip monitor address route 2>/dev/null | head -n1 >/dev/null' || true

		# On IP address/route changes, wait a few seconds more to ensure
		# the system has ample time to react and set things up for us.
		log "${name}: retrying ..."
		sleep 2
	    done
	elif ! "$script"; then
	    log "${name}: setup failed for local image $image"
	    rm -f "$pidfile"
	    exit 1
	fi

	rm -f "$pidfile"

	# Remove the old image if it's not used by any other containers
	if [ -n "$old_image_id" ]; then
	    # Check if the old image is still in use by any containers
	    old_image_id=${old_image_id:0:12}
	    if ! podman ps -a --format '{{.ImageID}}' | grep -q "^${old_image_id}"; then
		log "Removing old image $old_image_id"
		podman rmi "$old_image_id" 2>/dev/null || true
	    else
		log "Old image $old_image_id still in use by other containers, keeping it"
	    fi
	fi
	;;
    shell)
	if [ -z "$name" ]; then
	    name="$1"
	    shift
	fi
	if [ $# -gt 0 ]; then
	    podman exec -i "$name" sh -c "$*"
	else
	    podman exec -it "$name" sh -l
	fi
	;;
    show)
	cmd=$1
	[ -n "$cmd" ] && shift
	case $cmd in
	    image*)
		if [ -n "$simple" ]; then
		    podman images $all --format "{{.Names}} {{.Size}}" \
			| sed 's/\[\(.*\)\] /\1 /g' \
			| awk '{ printf "%-60s %s %s\n", $1, $2, $3}'
		else
		    podman images $all
		fi
		;;
	    volume*)
		printf "%-20s  CONTAINER\n" "VOLUME"
		for v in $(podman volume ls --format "{{.Name}}"); do
		    printf "%-20s" "$v"
		    podman ps -a --filter volume="$v"  --format '{{.Names}}' | sed 's/^/  /'
		done
		;;
	    *)
		if [ -n "$simple" ]; then
		    podman ps $all --format "{{.ID}}  {{.Names}}  {{.Image}}" \
			| awk '{ printf "%s  %-30s %s\n", $1, $2, $3}'
		else
		    podman ps $all
		fi
		;;
	esac
	;;
    start)
	if [ -n "$name" ]; then
	    wrap "$name" start
	elif [ -n "$1" ]; then
	    start "$1"
	else
	    usage
	    exit 1
	fi
	;;
    restart)
	if [ -n "$name" ]; then
	    wrap "$name" restart
	elif [ -n "$1" ]; then
	    cmd=$1
	    name=$2
	    if [ "$cmd" = "network" ] && [ -n "$name" ]; then
		netrestart "$name"
	    else
		name=$1
		stop "$name"
		while running "$name"; do
		    _=$((timeout -= 1))
		    if [ $timeout -le 0 ]; then
			err 1 "timed out waiting for container $1 to stop before restarting it."
		    fi
		    sleep 1
		done
		start "$name"
	    fi
	else
	    usage
	    exit 1
	fi
	;;
    stop)
	if [ -n "$name" ]; then
	    wrap "$name" stop
	elif [ -n "$1" ]; then
	    stop "$1"
	else
	    usage
	    exit 1
	fi
	;;
    stat*)
	podman stats -i 2
	;;
    upgrade)
	if [ -z "$name" ]; then
	    name="$1"
	fi
	script=/run/containers/${name}.sh

	# Verify container exists
	if ! podman container exists "$name"; then
	    echo "No such container: $name"
	    exit 1
	fi

	# Check if container uses a mutable tag (e.g., :latest)
	# Get the actual image name from the running container
	image_name=$(podman inspect "$name" | jq -r '.[].ImageName')
	if [ -z "$image_name" ]; then
	    echo "Cannot determine ImageName for container $name"
	    exit 1
	fi

	# Check if image uses :latest or other mutable tag
	# Images with immutable digests (@sha256:...) don't benefit from upgrade
	if echo "$image_name" | grep -qE '@sha256:'; then
	    echo "Container $name uses an immutable image digest ($image_name)"
	    echo "Upgrade will have no effect. Update the configuration to use a mutable tag."
	    exit 1
	fi

	# Get image URI from the container script
	img=$(awk '/^# meta-image:/ {print $3}' "$script")
	if [ -z "$img" ]; then
	    echo "Cannot determine image for container $name"
	    exit 1
	fi

	echo ">> Upgrading container $name from image $img ..."

	# Capture the current image ID before upgrade so we can remove it after
	old_image_id=$(podman inspect "$name" --format '{{.ImageID}}' 2>/dev/null)

	# Stop container using proper command (goes through Finit)
	printf ">> Stopping container ... "
	container stop "$name"
	echo "done"

	# If --reset-volumes requested, delete named volumes so they re-initialize from new image
	if [ -n "$reset_volumes" ]; then
	    printf ">> Resetting named volumes (all configuration will be lost): "
	    grep -oE -- '-v [^ ]+' "$script" | awk '{print $2}' | cut -d: -f1 | \
	    while read -r vol_name; do
		if podman volume exists "$vol_name" 2>/dev/null; then
		    printf "%s " "$vol_name"
		    log "Removing volume $vol_name"
		    podman volume rm -f "$vol_name" >/dev/null || true
		fi
	    done
	    echo "done"
	fi

	# Set force flag to ensure fresh pull/fetch of image
	export force="-f"

	# For remote images, force re-pull
	case "$img" in
	    docker://* | ftp://* | http://* | https://*)
		printf ">> Pulling latest image ... "
		if ! load_archive "$img" >/dev/null 2>&1; then
		    echo "failed"
		    echo "Failed fetching $img, check your network settings."
		    exit 1
		fi
		echo "done"
		;;
	    *)
		# Local archives - user must update the file manually
		echo ">> Using local image $img (ensure file is updated) ..."
		;;
	esac

	# Recreate container by running the script
	echo ">> Recreating container ..."
	if ! "$script"; then
	    force=
	    echo ">> Failed recreating container $name"
	    exit 1
	fi
	force=

	# Remove the old image if it's not used by any other containers
	if [ -n "$old_image_id" ]; then
	    # Check if the old image is still in use by any containers
	    old_image_id=${old_image_id:0:12}
	    if ! podman ps -a --format '{{.ImageID}}' | grep -q "^${old_image_id}"; then
		log "Removing old image $old_image_id"
		podman rmi "$old_image_id" 2>/dev/null || true
	    else
		log "Old image $old_image_id still in use by other containers, keeping it"
	    fi
	fi

	echo ">> Container $name upgraded successfully."
	;;
    volume)
	cmd=$1
	[ -n "$cmd" ] && shift
	case $cmd in
	    prune)
		podman volume prune $force
		;;
	    *)
		false
		;;
	esac
	;;
    *)
	if [ -n "$SERVICE_SCRIPT_TYPE" ] && [ -n "$SERVICE_ID" ]; then
	    case "$SERVICE_SCRIPT_TYPE" in
		pre)
		    # Called as pre-script from Finit service
		    exec $container -q -n "$SERVICE_ID" setup
		    ;;
		cleanup)
		    # Called as cleanup-script from Finit service
		    log "Calling $container -n $SERVICE_ID cleanup"
		    exec $container -q -n "$SERVICE_ID" cleanup
		    ;;
		*)
		    false
		    ;;
	    esac
	fi
	usage
	exit 1
	;;
esac
