#!/bin/bash
# CloudNVR host operator command.
#
# The active release is always releases/current. Compose never resolves a
# mutable channel tag. Updates stage exact image digests, prove health, and
# move the current pointer only after activation succeeds.
set -Eeuo pipefail

ROOT="${CLOUDNVR_ROOT:-/opt/dividia}"
ENV_FILE="$ROOT/.env"
MARKER_FILE="$ROOT/.product"
RELEASES_DIR="$ROOT/releases"
STATE_DIR="$ROOT/state"
BIN_DIR="$ROOT/bin"
CURRENT_LINK="$RELEASES_DIR/current"
DEFAULT_LOCK_FILE=/run/lock/cloudnvr/mutation.lock
LOCK_FILE="${CLOUDNVR_LOCK_FILE:-$DEFAULT_LOCK_FILE}"
CRON_FILE="${CLOUDNVR_CRON_FILE:-/etc/cron.d/dividia-cloudnvr-update}"
SERVICE_FILE="${CLOUDNVR_SERVICE_FILE:-/etc/systemd/system/cloudnvr.service}"
CMD_LINK="${CLOUDNVR_CMD_LINK:-/usr/local/bin/nvr}"
DEFAULT_BUNDLE_BASE_URL="https://files.dividia.net/software/cloudnvr-docker"
DRAGON_DEADLINE="${CLOUDNVR_DRAGON_ACTIVATION_DEADLINE_SEC:-300}"
CONNECTOR_DEADLINE="${CLOUDNVR_CONNECTOR_ACTIVATION_DEADLINE_SEC:-120}"
HEALTH_POLL="${CLOUDNVR_HEALTH_POLL_SEC:-2}"
SERVICE_STOP_LOCK_TIMEOUT="${CLOUDNVR_SERVICE_STOP_LOCK_TIMEOUT_SEC:-480}"

log() { printf '%s\n' "$*"; }
warn() { printf 'WARNING: %s\n' "$*" >&2; }
die() { printf 'ERROR: %s\n' "$*" >&2; return 1; }

sha256_file() {
    if command -v sha256sum >/dev/null 2>&1; then
        sha256sum "$1" | awk '{print $1}'
    else
        shasum -a 256 "$1" | awk '{print $1}'
    fi
}

file_mode() {
    python3 -c 'import os,stat,sys; print(format(stat.S_IMODE(os.lstat(sys.argv[1]).st_mode), "o"))' "$1"
}

require_root() {
    if [[ "${CLOUDNVR_ALLOW_NON_ROOT:-0}" != 1 && "$EUID" -ne 0 ]]; then
        die "this command requires root"
    fi
}

channel_is_valid() {
    [[ "$1" =~ ^(dev(-[a-z0-9-]+)?|[0-9]{4}\.(1[0-2]|[1-9])\.[1-9][0-9]*)$ ]]
}

digest_is_valid() {
    [[ "$1" =~ ^sha256:[0-9a-f]{64}$ ]]
}

release_id_is_valid() {
    [[ "$1" =~ ^[0-9a-f]{64}$ ]]
}

marker_is_ours() {
    [[ -f "$MARKER_FILE" ]] || return 1
    [[ "$(kv_get "$MARKER_FILE" product 2>/dev/null || true)" == cloudnvr ]] || return 1
    [[ "$(kv_get "$MARKER_FILE" schema 2>/dev/null || true)" == 1 ]]
}

require_install() {
    if ! marker_is_ours && [[ "${CLOUDNVR_INSTALLING:-0}" != 1 ]]; then
        die "$ROOT is not a CloudNVR schema 1 installation"
    fi
}

kv_get() {
    local file="$1" wanted="$2" key value found=""
    [[ -f "$file" ]] || return 1
    while IFS='=' read -r key value || [[ -n "$key$value" ]]; do
        if [[ "$key" == "$wanted" ]]; then
            found="$value"
        fi
    done < "$file"
    [[ -n "$found" ]] || return 1
    printf '%s\n' "$found"
}

load_host_env() {
    [[ -f "$ENV_FILE" ]] || { die "missing $ENV_FILE"; return 1; }
    CHANNEL="$(kv_get "$ENV_FILE" CHANNEL)" || { die "missing CHANNEL in $ENV_FILE"; return 1; }
    PLATFORM="$(kv_get "$ENV_FILE" PLATFORM)" || { die "missing PLATFORM in $ENV_FILE"; return 1; }
    REGISTRY="$(kv_get "$ENV_FILE" REGISTRY)" || { die "missing REGISTRY in $ENV_FILE"; return 1; }
    COMPOSE_PROJECT_NAME="$(kv_get "$ENV_FILE" COMPOSE_PROJECT_NAME)" \
        || { die "missing COMPOSE_PROJECT_NAME in $ENV_FILE"; return 1; }
    BUNDLE_BASE_URL="$(kv_get "$ENV_FILE" BUNDLE_BASE_URL 2>/dev/null || true)"
    BUNDLE_BASE_URL="${BUNDLE_BASE_URL:-$DEFAULT_BUNDLE_BASE_URL}"
    channel_is_valid "$CHANNEL" || { die "invalid configured channel: $CHANNEL"; return 1; }
    [[ "$PLATFORM" == linux/arm64 ]] || { die "unsupported configured platform: $PLATFORM"; return 1; }
    [[ "$REGISTRY" == docker.io ]] || { die "unsupported configured registry: $REGISTRY"; return 1; }
    [[ "$COMPOSE_PROJECT_NAME" =~ ^[a-z0-9][a-z0-9_-]*$ ]] \
        || { die "unsafe Compose project name"; return 1; }
}

atomic_write() {
    local path="$1" mode="${2:-0644}"
    python3 -c '
import os, pathlib, sys, tempfile
path = pathlib.Path(sys.argv[1])
mode = int(sys.argv[2], 8)
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp = tempfile.mkstemp(prefix="." + path.name + ".", dir=str(path.parent))
try:
    data = sys.stdin.buffer.read()
    with os.fdopen(fd, "wb") as out:
        out.write(data)
        out.flush()
        os.fsync(out.fileno())
    os.chmod(tmp, mode)
    os.replace(tmp, path)
    dfd = os.open(path.parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
    try: os.fsync(dfd)
    finally: os.close(dfd)
except BaseException:
    try: os.unlink(tmp)
    except FileNotFoundError: pass
    raise
' "$path" "$mode"
}

durable_sync_dirs() {
    python3 - "$@" <<'PY'
import os, pathlib, stat, sys

fail = os.environ.get("CLOUDNVR_TEST_DIR_FSYNC_FAIL", "")
paths = set()
for raw in sys.argv[1:]:
    path = pathlib.Path(raw)
    info = os.lstat(path)
    if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode):
        raise SystemExit("durability target is not a real directory: %s" % path)
    paths.add(path)
    paths.add(path.parent)

for path in sorted(paths, key=lambda item: len(item.parts), reverse=True):
    if fail and (str(path) == fail or path.name == fail):
        raise OSError("injected directory sync failure: %s" % path)
    fd = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
    try:
        os.fsync(fd)
    finally:
        os.close(fd)
PY
}

atomic_copy() {
    local source="$1" target="$2" mode="${3:-0644}"
    [[ -f "$source" && ! -L "$source" && -s "$source" ]] \
        || { die "invalid source file: $source"; return 1; }
    atomic_write "$target" "$mode" < "$source"
}

atomic_symlink() {
    local directory="$1" name="$2" target="$3"
    python3 -c '
import os, pathlib, sys
directory = pathlib.Path(sys.argv[1])
name, target = sys.argv[2], sys.argv[3]
directory.mkdir(parents=True, exist_ok=True)
tmp = directory / ("." + name + ".new.%d" % os.getpid())
committed = False
try:
    os.symlink(target, tmp)
    os.replace(tmp, directory / name)
    committed = True
    dfd = os.open(directory, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
    try:
        if os.environ.get("CLOUDNVR_TEST_SYMLINK_DIR_FSYNC_FAIL") == name:
            raise OSError("injected symlink directory sync failure")
        os.fsync(dfd)
    except OSError:
        raise SystemExit(2)
    finally: os.close(dfd)
except BaseException:
    if not committed:
        try: tmp.unlink()
        except FileNotFoundError: pass
    raise
' "$directory" "$name" "$target"
}

fsync_tree() {
    [[ "${CLOUDNVR_TEST_FSYNC_TREE_FAIL:-0}" != 1 ]] || return 1
    python3 -c '
import os, pathlib, sys
root = pathlib.Path(sys.argv[1])
for path in sorted(root.rglob("*")):
    if path.is_file() and not path.is_symlink():
        fd = os.open(path, os.O_RDONLY)
        try: os.fsync(fd)
        finally: os.close(fd)
for path in sorted((p for p in root.rglob("*") if p.is_dir()), reverse=True):
    fd = os.open(path, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
    try: os.fsync(fd)
    finally: os.close(fd)
fd = os.open(root, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try: os.fsync(fd)
finally: os.close(fd)
' "$1"
}

manifest_load() {
    local file="$1" parsed
    parsed="$(python3 - "$file" <<'PY'
import hashlib, json, re, sys
path = sys.argv[1]
with open(path, encoding="utf-8") as handle:
    value = json.load(handle)
if value.get("schema") != 1 or value.get("product") != "cloudnvr":
    raise SystemExit("wrong product or schema")
images = value.get("images")
if not isinstance(images, dict):
    raise SystemExit("missing images")
dragon = images.get("dragon", {})
connector = images.get("connector", {})
fields = [
    value.get("release_id", ""), value.get("platform", ""),
    value.get("source_revision", ""),
    value.get("connector_nvr_source_revision", ""),
    dragon.get("repository", ""), dragon.get("digest", ""),
    dragon.get("reference", ""), dragon.get("revision_tag", ""),
    connector.get("repository", ""), connector.get("digest", ""),
    connector.get("reference", ""), connector.get("revision_tag", ""),
    value.get("parent_release_id") or "",
]
if any(not isinstance(item, str) or "\t" in item or "\n" in item for item in fields):
    raise SystemExit("invalid release field")
rid, platform, source, nvr, dr, dd, dref, dtag, cr, cd, cref, ctag, parent = fields
if not re.fullmatch(r"[0-9a-f]{64}", rid): raise SystemExit("bad release id")
if platform != "linux/arm64": raise SystemExit("bad platform")
if not re.fullmatch(r"[0-9a-f]{40}", source): raise SystemExit("bad source revision")
if not re.fullmatch(r"[0-9a-f]{40}", nvr): raise SystemExit("bad NVR revision")
if dr != "docker.io/dividia/cloudnvr-dragon": raise SystemExit("bad Dragon repository")
if cr != "docker.io/dividia/cloudnvr-connector": raise SystemExit("bad connector repository")
if not re.fullmatch(r"sha256:[0-9a-f]{64}", dd): raise SystemExit("bad Dragon digest")
if not re.fullmatch(r"sha256:[0-9a-f]{64}", cd): raise SystemExit("bad connector digest")
if dref != dr + "@" + dd or cref != cr + "@" + cd: raise SystemExit("bad digest reference")
if not re.fullmatch(r"[A-Za-z0-9._/:@-]+", dtag): raise SystemExit("bad Dragon revision tag")
if not re.fullmatch(r"[A-Za-z0-9._/:@-]+", ctag): raise SystemExit("bad connector revision tag")
if parent and not re.fullmatch(r"[0-9a-f]{64}", parent): raise SystemExit("bad parent")
if parent: raise SystemExit("release manifests cannot contain activation lineage")
canonical = "\n".join([
    "schema=1", "product=cloudnvr", "platform=" + platform,
    "source_revision=" + source, "nvr_source_revision=" + nvr,
    "dragon=" + dr + "@" + dd, "connector=" + cr + "@" + cd, "",
])
if hashlib.sha256(canonical.encode()).hexdigest() != rid:
    raise SystemExit("release identity mismatch")
print("\t".join(fields))
PY
)" || { die "invalid release manifest: $file"; return 1; }
    # shellcheck disable=SC2034 # Repository and revision-tag fields are validated by the parser.
    IFS=$'\t' read -r \
        MANIFEST_RELEASE_ID MANIFEST_PLATFORM MANIFEST_SOURCE_REV MANIFEST_NVR_REV \
        MANIFEST_DRAGON_REPO MANIFEST_DRAGON_DIGEST MANIFEST_DRAGON_REF MANIFEST_DRAGON_TAG \
        MANIFEST_CONNECTOR_REPO MANIFEST_CONNECTOR_DIGEST MANIFEST_CONNECTOR_REF MANIFEST_CONNECTOR_TAG \
        MANIFEST_PARENT <<< "$parsed"
}

current_release_id() {
    local target
    [[ -L "$CURRENT_LINK" ]] || return 1
    target="$(readlink "$CURRENT_LINK")" || return 1
    release_id_is_valid "$target" || return 1
    [[ -d "$RELEASES_DIR/$target" && ! -L "$RELEASES_DIR/$target" ]] || return 1
    printf '%s\n' "$target"
}

release_path() {
    local release_id="$1"
    release_id_is_valid "$release_id" || return 1
    printf '%s/%s\n' "$RELEASES_DIR" "$release_id"
}

compose_cmd() {
    local release_id="$1"
    shift
    local dir
    dir="$(release_path "$release_id")" || return 1
    [[ -f "$dir/release.env" && -f "$dir/docker-compose.yml" ]] \
        || { die "release $release_id is incomplete"; return 1; }
    docker compose --project-directory "$ROOT" --env-file "$ROOT/.env" \
        --env-file "$dir/release.env" \
        -f "$dir/docker-compose.yml" "$@"
}

candidate_compose_cmd() {
    local release_id="$1"
    shift
    local dir
    dir="$(release_path "$release_id")" || return 1
    [[ -f "$dir/release.env" && -f "$dir/docker-compose.yml" \
       && -f "$dir/activation-compose.yml" ]] \
        || { die "release $release_id is incomplete"; return 1; }
    docker compose --project-directory "$ROOT" --env-file "$ROOT/.env" \
        --env-file "$dir/release.env" \
        -f "$dir/docker-compose.yml" -f "$dir/activation-compose.yml" "$@"
}

container_health() {
    local release_id="$1" service="$2" cid state expected actual digest_key repository
    cid="$(compose_cmd "$release_id" ps -q "$service" 2>/dev/null || true)"
    [[ -n "$cid" ]] || { printf 'missing\n'; return 1; }
    if [[ "$service" == dragon ]]; then
        digest_key=DRAGON_DIGEST
        repository=docker.io/dividia/cloudnvr-dragon
    else
        digest_key=CONNECTOR_DIGEST
        repository=docker.io/dividia/cloudnvr-connector
    fi
    expected="$repository@$(kv_get "$RELEASES_DIR/$release_id/release.env" "$digest_key" 2>/dev/null || true)"
    actual="$(docker inspect --format '{{.Config.Image}}' "$cid" 2>/dev/null || true)"
    if [[ "$actual" != "$expected" ]]; then
        printf 'wrong-image\n'
        return 1
    fi
    state="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "$cid" 2>/dev/null || true)"
    printf '%s\n' "${state:-unknown}"
    [[ "$state" == healthy ]]
}

release_services_present() {
    local release_id="$1" service
    for service in dragon connector; do
        [[ -n "$(compose_cmd "$release_id" ps -q "$service" 2>/dev/null || true)" ]] || return 1
    done
}

prepare_lock_file() {
    python3 - "$LOCK_FILE" "${CLOUDNVR_ALLOW_NON_ROOT:-0}" "$DEFAULT_LOCK_FILE" <<'PY'
import os, pathlib, stat, sys

path = pathlib.Path(sys.argv[1])
allow_non_root = sys.argv[2] == "1"
default_path = pathlib.Path(sys.argv[3])
if not path.is_absolute():
    raise SystemExit("CloudNVR lock path must be absolute")
if not allow_non_root and path != default_path:
    raise SystemExit("CloudNVR lock overrides are test-only")
parent = path.parent
parent.mkdir(parents=True, exist_ok=True, mode=0o700)
parent_stat = os.lstat(parent)
if not stat.S_ISDIR(parent_stat.st_mode) or stat.S_ISLNK(parent_stat.st_mode):
    raise SystemExit("CloudNVR lock directory is not a real directory")
if not allow_non_root:
    if parent_stat.st_uid != 0:
        raise SystemExit("CloudNVR lock directory is not root-owned")
    os.chmod(parent, 0o700)
if os.path.lexists(path):
    lock_stat = os.lstat(path)
    if not stat.S_ISREG(lock_stat.st_mode) or stat.S_ISLNK(lock_stat.st_mode):
        raise SystemExit("CloudNVR lock file is not a regular file")
    if not allow_non_root and lock_stat.st_uid != 0:
        raise SystemExit("CloudNVR lock file is not root-owned")
else:
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
    flags |= getattr(os, "O_NOFOLLOW", 0)
    fd = os.open(path, flags, 0o600)
    os.close(fd)
if not allow_non_root:
    os.chmod(path, 0o600)
PY
}

acquire_mutation_lock() {
    prepare_lock_file || { die "unsafe CloudNVR mutation lock: $LOCK_FILE"; return 1; }
    if [[ "${CLOUDNVR_INHERITED_LOCK:-0}" == 1 ]]; then
        : >&9 2>/dev/null \
            || { die "the inherited CloudNVR mutation lock is unavailable"; return 1; }
        flock -n 9 \
            || { die "the inherited CloudNVR mutation lock is not owned"; return 1; }
        return 0
    fi
    exec 9>>"$LOCK_FILE"
    flock -n 9 || { die "another CloudNVR mutation owns the lock"; return 1; }
}

acquire_service_lock() {
    local action="$1"
    [[ "$SERVICE_STOP_LOCK_TIMEOUT" =~ ^[1-9][0-9]*$ ]] \
        || { die "invalid service lock timeout"; return 1; }
    prepare_lock_file || { die "unsafe CloudNVR mutation lock: $LOCK_FILE"; return 1; }
    exec 9>>"$LOCK_FILE"
    flock -w "$SERVICE_STOP_LOCK_TIMEOUT" 9 \
        || { die "CloudNVR service $action timed out waiting for the mutation lock"; return 1; }
}

systemd_active_status() {
    local state
    state="$(systemctl show -p ActiveState --value "$1" 2>/dev/null)" || return 2
    case "$state" in
        active|activating|reloading|deactivating) return 0 ;;
        inactive|failed) return 1 ;;
        *) return 2 ;;
    esac
}

systemd_inactive_verified() {
    local rc
    if systemd_active_status "$1"; then
        return 1
    else
        rc=$?
    fi
    [[ "$rc" -eq 1 ]]
}

wait_service() {
    local release_id="$1" service="$2" deadline="$3" started="$SECONDS" health
    while :; do
        health="$(container_health "$release_id" "$service" || true)"
        if [[ "$health" == healthy ]]; then
            log "$service is healthy"
            return 0
        fi
        if (( SECONDS - started >= deadline )); then
            die "$service did not become healthy within ${deadline}s (last state: $health)"
            return 1
        fi
        sleep "$HEALTH_POLL"
    done
}

wait_release() {
    local release_id="$1"
    wait_service "$release_id" dragon "$DRAGON_DEADLINE" || return 1
    wait_service "$release_id" connector "$CONNECTOR_DEADLINE"
}

dragon_activation_ready() {
    local release_id="$1" cid health
    health="$(container_health "$release_id" dragon || true)"
    [[ "$health" == healthy ]] || return 1
    cid="$(compose_cmd "$release_id" ps -q dragon 2>/dev/null || true)"
    [[ -n "$cid" ]] || return 1
    docker exec "$cid" /usr/local/bin/dragon-healthcheck --activation \
        >/dev/null 2>&1
}

wait_dragon_activation() {
    local release_id="$1" started="$SECONDS"
    while :; do
        if dragon_activation_ready "$release_id"; then
            log "dragon activation is ready"
            return 0
        fi
        if (( SECONDS - started >= DRAGON_DEADLINE )); then
            die "dragon did not reach activation readiness within ${DRAGON_DEADLINE}s"
            return 1
        fi
        sleep "$HEALTH_POLL"
    done
}

image_label() {
    docker image inspect --format "{{index .Config.Labels \"$2\"}}" "$1" 2>/dev/null
}

verify_image() {
    local ref="$1" product="$2" expected_source="$3" expected_nvr="${4:-}"
    local architecture title revision profile nvr_revision version
    architecture="$(docker image inspect --format '{{.Architecture}}' "$ref" 2>/dev/null || true)"
    [[ "$architecture" == arm64 ]] || { die "$product image architecture is '$architecture', expected arm64"; return 1; }
    title="$(image_label "$ref" org.opencontainers.image.title || true)"
    revision="$(image_label "$ref" org.opencontainers.image.revision || true)"
    version="$(image_label "$ref" org.opencontainers.image.version || true)"
    [[ "$title" == "$product" ]] || { die "$ref has product label '$title'"; return 1; }
    [[ "$revision" == "$expected_source" ]] || { die "$ref has source revision '$revision'"; return 1; }
    [[ -n "$version" ]] || { die "$ref has no version label"; return 1; }
    if [[ "$product" == cloudnvr-dragon ]]; then
        [[ "$(image_label "$ref" net.dividia.cloudnvr.platform || true)" == jetson-arm64 ]] \
            || { die "$ref is not a Jetson image"; return 1; }
        profile="$(image_label "$ref" net.dividia.cloudnvr.profile || true)"
        [[ "$profile" == production ]] || { die "$ref is not a strict production image"; return 1; }
    else
        nvr_revision="$(image_label "$ref" net.dividia.cloudnvr.nvr-source-rev || true)"
        [[ "$nvr_revision" == "$expected_nvr" ]] \
            || { die "$ref has connector NVR revision '$nvr_revision'"; return 1; }
    fi
}

extract_host_tools() {
    local image="$1" destination="$2" cid="" name rc=0
    mkdir -p "$destination"
    cid="$(docker create "$image")" || { die "cannot create a tool-extraction container"; return 1; }
    for name in docker-compose.yml install-cloudnvr.sh nvr cloudnvr.service; do
        if ! docker cp "$cid:/usr/share/cloudnvr/$name" "$destination/$name" >/dev/null; then
            die "candidate image is missing /usr/share/cloudnvr/$name"
            rc=1
            break
        fi
    done
    docker rm -f "$cid" >/dev/null 2>&1 || true
    [[ "$rc" -eq 0 ]] || return "$rc"
    for name in docker-compose.yml install-cloudnvr.sh nvr cloudnvr.service; do
        [[ -f "$destination/$name" && ! -L "$destination/$name" && -s "$destination/$name" ]] \
            || { die "invalid extracted host tool: $name"; return 1; }
    done
    bash -n "$destination/nvr" || { die "candidate nvr script failed bash -n"; return 1; }
    bash -n "$destination/install-cloudnvr.sh" \
        || { die "candidate installer failed bash -n"; return 1; }
}

write_release_env() {
    local path="$1" dragon_digest="$2" connector_digest="$3"
    {
        printf 'DRAGON_DIGEST=%s\n' "$dragon_digest"
        printf 'CONNECTOR_DIGEST=%s\n' "$connector_digest"
        printf 'COMPOSE_PROJECT_NAME=%s\n' "$COMPOSE_PROJECT_NAME"
    } | atomic_write "$path" 0600
}

write_activation_compose() {
    printf '%s\n' \
        'services:' \
        '  dragon:' \
        '    restart: "no"' \
        '  connector:' \
        '    restart: "no"' | atomic_write "$1" 0644
}

validate_compose() {
    local tools="$1" env_file="$2" images services rendered
    services="$(docker compose --project-directory "$ROOT" --env-file "$ROOT/.env" \
        --env-file "$env_file" \
        -f "$tools/docker-compose.yml" config --services 2>/dev/null | sort)" \
        || { die "candidate Compose file does not render"; return 1; }
    [[ "$services" == $'connector\ndragon' ]] \
        || { die "candidate Compose must contain exactly dragon and connector"; return 1; }
    images="$(docker compose --project-directory "$ROOT" --env-file "$ROOT/.env" \
        --env-file "$env_file" \
        -f "$tools/docker-compose.yml" config --images 2>/dev/null | sort -u)" \
        || { die "candidate Compose image list failed"; return 1; }
    [[ "$images" == "$MANIFEST_CONNECTOR_REF"$'\n'"$MANIFEST_DRAGON_REF" ]] \
        || { die "candidate Compose does not use the manifest digests"; return 1; }
    rendered="$(docker compose --project-directory "$ROOT" --env-file "$ROOT/.env" \
        --env-file "$env_file" \
        -f "$tools/docker-compose.yml" config 2>/dev/null)" \
        || { die "candidate Compose file does not render"; return 1; }
    if grep -Eq '^[[:space:]]+build:' <<< "$rendered"; then
        die "candidate Compose contains a build directive"
        return 1
    fi
}

prepare_release() {
    local manifest="$1" tools="$2" target staging name expected_activation
    target="$RELEASES_DIR/$MANIFEST_RELEASE_ID"
    if [[ -e "$target" ]]; then
        [[ -d "$target" && ! -L "$target" && "$(file_mode "$target")" == 700 ]] \
            || { die "release target is not a private directory: $target"; return 1; }
        cmp -s "$manifest" "$target/release.json" || {
            die "installed release metadata does not match candidate $MANIFEST_RELEASE_ID"
            return 1
        }
        for name in release.json docker-compose.yml activation-compose.yml cloudnvr.service; do
            [[ -f "$target/$name" && ! -L "$target/$name" \
               && "$(file_mode "$target/$name")" == 644 ]] \
                || { die "installed $name has an invalid type or mode"; return 1; }
        done
        [[ -f "$target/release.env" && ! -L "$target/release.env" \
           && "$(file_mode "$target/release.env")" == 600 ]] \
            || { die "installed release.env has an invalid type or mode"; return 1; }
        for name in install-cloudnvr.sh nvr; do
            [[ -f "$target/$name" && ! -L "$target/$name" \
               && "$(file_mode "$target/$name")" == 755 ]] \
                || { die "installed $name has an invalid type or mode"; return 1; }
        done
        for name in docker-compose.yml install-cloudnvr.sh nvr cloudnvr.service; do
            cmp -s "$tools/$name" "$target/$name" \
                || { die "installed $name differs from candidate $MANIFEST_RELEASE_ID"; return 1; }
        done
        expected_activation="$(printf '%s\n' 'services:' '  dragon:' '    restart: "no"' '  connector:' '    restart: "no"')"
        [[ -f "$target/activation-compose.yml" \
           && "$(cat "$target/activation-compose.yml")" == "$expected_activation" ]] \
            || { die "installed activation Compose file differs from the release contract"; return 1; }
        python3 - "$target/release.env" "$MANIFEST_DRAGON_DIGEST" \
            "$MANIFEST_CONNECTOR_DIGEST" "$COMPOSE_PROJECT_NAME" <<'PY' || {
import pathlib, sys
path, dragon, connector, project = sys.argv[1:]
expected = (
    f"DRAGON_DIGEST={dragon}\n"
    f"CONNECTOR_DIGEST={connector}\n"
    f"COMPOSE_PROJECT_NAME={project}\n"
).encode()
if pathlib.Path(path).read_bytes() != expected:
    raise SystemExit("installed release.env differs from the release contract")
PY
            die "installed release.env differs from candidate $MANIFEST_RELEASE_ID"
            return 1
        }
        manifest_load "$target/release.json" || return 1
        [[ "$MANIFEST_RELEASE_ID" == "${target##*/}" ]] || return 1
        validate_compose "$target" "$target/release.env"
        return
    fi

    staging="$RELEASES_DIR/.staging-$MANIFEST_RELEASE_ID-$$"
    [[ ! -e "$staging" ]] || { die "staging path already exists: $staging"; return 1; }
    mkdir -m 0700 "$staging" || return 1
    atomic_copy "$manifest" "$staging/release.json" 0644 || return 1
    atomic_copy "$tools/docker-compose.yml" "$staging/docker-compose.yml" 0644 || return 1
    atomic_copy "$tools/install-cloudnvr.sh" "$staging/install-cloudnvr.sh" 0755 || return 1
    atomic_copy "$tools/nvr" "$staging/nvr" 0755 || return 1
    atomic_copy "$tools/cloudnvr.service" "$staging/cloudnvr.service" 0644 || return 1
    write_activation_compose "$staging/activation-compose.yml" || return 1
    write_release_env "$staging/release.env" "$MANIFEST_DRAGON_DIGEST" "$MANIFEST_CONNECTOR_DIGEST" \
        || return 1
    validate_compose "$staging" "$staging/release.env" || return 1
    fsync_tree "$staging" || return 1
    python3 -c '
import os, sys
source, target, parent = sys.argv[1:]
os.replace(source, target)
fd = os.open(parent, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0))
try: os.fsync(fd)
finally: os.close(fd)
' "$staging" "$target" "$RELEASES_DIR" || return 1
}

cron_minute() {
    local seed
    [[ "${CLOUDNVR_TEST_CRON_MINUTE_FAIL:-0}" != 1 ]] || return 1
    if [[ -s "${CLOUDNVR_DEVICE_ID_FILE:-/etc/machine-id}" ]]; then
        seed="$(tr -d '[:space:]' < "${CLOUDNVR_DEVICE_ID_FILE:-/etc/machine-id}")"
    else
        seed="$(hostname 2>/dev/null || printf cloudnvr)"
    fi
    printf '%s' "$seed" | cksum | awk '{print $1 % 60}'
}

ensure_cron() {
    [[ "${CLOUDNVR_SKIP_HOST_ARTIFACTS:-0}" != 1 ]] || return 0
    local minute body
    minute="$(cron_minute)" || return 1
    body="# Daily CloudNVR update. Managed by /opt/dividia/bin/nvr.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$minute 2 * * * root $CMD_LINK update >>$ROOT/data/logs/update.log 2>&1
"
    printf '%s' "$body" | atomic_write "$CRON_FILE" 0644
}

prepare_host_dispatch() {
    local release_id="$1" tools="$2" current=""
    mkdir -p "$BIN_DIR" "$ROOT/data/logs" || return 1
    durable_sync_dirs "$BIN_DIR" "$ROOT/data/logs" || return 1
    [[ -s "$RELEASES_DIR/$release_id/nvr" \
       && -s "$RELEASES_DIR/$release_id/install-cloudnvr.sh" \
       && -s "$RELEASES_DIR/$release_id/cloudnvr.service" ]] \
        || { die "release $release_id is missing its host tools"; return 1; }
    current="$(current_release_id 2>/dev/null || true)"
    if [[ -n "$current" ]]; then
        [[ -s "$RELEASES_DIR/$current/nvr" \
           && -s "$RELEASES_DIR/$current/install-cloudnvr.sh" \
           && -s "$RELEASES_DIR/$current/cloudnvr.service" ]] \
            || { die "current release $current is missing its host tools"; return 1; }
        atomic_symlink "$BIN_DIR" nvr ../releases/current/nvr || return 1
        atomic_symlink "$BIN_DIR" install-cloudnvr.sh ../releases/current/install-cloudnvr.sh || return 1
    else
        # The first install has no current pointer yet. Keep complete matching
        # bootstrap tools available until the pointer commits.
        atomic_copy "$tools/nvr" "$BIN_DIR/nvr" 0755 || return 1
        atomic_copy "$tools/install-cloudnvr.sh" "$BIN_DIR/install-cloudnvr.sh" 0755 || return 1
    fi
    atomic_symlink "$ROOT" docker-compose.yml releases/current/docker-compose.yml || return 1
    if [[ "${CLOUDNVR_SKIP_HOST_ARTIFACTS:-0}" != 1 ]]; then
        if [[ -n "$current" ]]; then
            atomic_symlink "${SERVICE_FILE%/*}" "${SERVICE_FILE##*/}" \
                "$RELEASES_DIR/current/cloudnvr.service" || return 1
        else
            atomic_copy "$tools/cloudnvr.service" "$SERVICE_FILE" 0644 || return 1
        fi
        atomic_symlink "${CMD_LINK%/*}" "${CMD_LINK##*/}" "$BIN_DIR/nvr" || return 1
        ensure_cron || return 1
        systemctl daemon-reload || return 1
        systemctl enable cloudnvr.service >/dev/null || return 1
        systemctl is-enabled --quiet cloudnvr.service || return 1
    fi
}

commit_host_dispatch() {
    atomic_symlink "$BIN_DIR" nvr ../releases/current/nvr || return 1
    atomic_symlink "$BIN_DIR" install-cloudnvr.sh ../releases/current/install-cloudnvr.sh || return 1
    if [[ "${CLOUDNVR_SKIP_HOST_ARTIFACTS:-0}" != 1 ]]; then
        atomic_symlink "${SERVICE_FILE%/*}" "${SERVICE_FILE##*/}" \
            "$RELEASES_DIR/current/cloudnvr.service" || return 1
        systemctl daemon-reload || return 1
        systemctl enable cloudnvr.service >/dev/null || return 1
        systemctl is-enabled --quiet cloudnvr.service || return 1
    fi
}

record_release_parent() {
    local release_id="$1" parent="$2"
    local path="$STATE_DIR/release-parents/$release_id"
    [[ "${CLOUDNVR_TEST_RELEASE_PARENT_FAIL:-}" != "$release_id" ]] || return 1
    release_id_is_valid "$release_id" || return 1
    [[ -z "$parent" ]] || release_id_is_valid "$parent" || return 1
    mkdir -p "${path%/*}" || return 1
    durable_sync_dirs "${path%/*}" || return 1
    printf '%s\n' "$parent" | atomic_write "$path" 0644
}

release_parent_id() {
    local release_id="$1" parent
    release_id_is_valid "$release_id" || return 1
    parent="$(head -n 1 "$STATE_DIR/release-parents/$release_id" 2>/dev/null || true)"
    release_id_is_valid "$parent" || return 1
    printf '%s\n' "$parent"
}

json_state() {
    local file="$1" result="$2" action="$3" release_id="$4" stage="$5" reason="$6"
    [[ "${CLOUDNVR_TEST_STATE_WRITE_FAIL:-}" != "$result" ]] || return 1
    python3 -c '
import json, sys
from datetime import datetime, timezone
value = {
  "schema": 1, "result": sys.argv[2], "action": sys.argv[3],
  "release_id": sys.argv[4] or None, "stage": sys.argv[5] or None,
  "reason": sys.argv[6] or None,
  "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
}
json.dump(value, sys.stdout, sort_keys=True, indent=2)
sys.stdout.write("\n")
' "$file" "$result" "$action" "$release_id" "$stage" "$reason" | atomic_write "$file" 0644
}

mark_rejected() {
    local release_id="$1" stage="$2" reason="$3"
    json_state "$STATE_DIR/rejected-release.json" rejected update "$release_id" "$stage" "$reason"
}

rejected_release_id() {
    [[ -f "$STATE_DIR/rejected-release.json" ]] || return 1
    python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("release_id") or "")' \
        "$STATE_DIR/rejected-release.json" 2>/dev/null
}

release_was_committed() {
    local wanted="$1"
    [[ -f "$STATE_DIR/accepted-releases/$wanted" ]]
}

mark_release_accepted() {
    local release_id="$1"
    release_id_is_valid "$release_id" || return 1
    mkdir -p "$STATE_DIR/accepted-releases" || return 1
    durable_sync_dirs "$STATE_DIR/accepted-releases" || return 1
    printf 'accepted=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
        | atomic_write "$STATE_DIR/accepted-releases/$release_id" 0644
}

clear_rejected() {
    local rejected
    [[ "${CLOUDNVR_TEST_CLEAR_REJECTED_FAIL:-0}" != 1 ]] || return 1
    rejected="$(rejected_release_id 2>/dev/null || true)"
    if [[ -n "$rejected" && "$rejected" == "$1" ]]; then
        rm -f "$STATE_DIR/rejected-release.json" || return 1
        python3 -c 'import os,sys; fd=os.open(sys.argv[1], os.O_RDONLY|getattr(os,"O_DIRECTORY",0)); os.fsync(fd); os.close(fd)' "$STATE_DIR"
    fi
}

set_hold() {
    local reason="${1:-operator}"
    [[ "${CLOUDNVR_TEST_HOLD_FAIL:-0}" != 1 ]] || return 1
    python3 -c '
import json,sys
from datetime import datetime, timezone
json.dump({"schema":1,"enabled":True,"reason":sys.argv[1],"timestamp":datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}, sys.stdout, sort_keys=True, indent=2)
sys.stdout.write("\n")
' "$reason" | atomic_write "$STATE_DIR/update-hold" 0644
}

disable_update_cron() {
    [[ -e "$CRON_FILE" || -L "$CRON_FILE" ]] || return 0
    rm -f "$CRON_FILE" || return 1
    python3 -c 'import os,sys; fd=os.open(sys.argv[1], os.O_RDONLY|getattr(os,"O_DIRECTORY",0)); os.fsync(fd); os.close(fd)' \
        "${CRON_FILE%/*}"
}

reject_release() {
    local release_id="$1" stage="$2" reason="$3"
    if mark_rejected "$release_id" "$stage" "$reason"; then
        return 0
    fi
    warn "could not record rejected release $release_id; enabling the update hold"
    if ! set_hold rejection-state-failure; then
        warn "could not enable the update hold after rejection-state failure; removing automatic update cron"
        if ! disable_update_cron; then
            warn "could not remove automatic update cron after rejection-state failure"
        fi
    fi
    return 1
}

clear_hold() {
    rm -f "$STATE_DIR/update-hold"
    python3 -c 'import os,sys; fd=os.open(sys.argv[1], os.O_RDONLY|getattr(os,"O_DIRECTORY",0)); os.fsync(fd); os.close(fd)' "$STATE_DIR"
}

maybe_power_loss() {
    if [[ "${CLOUDNVR_TEST_POWER_LOSS_AT:-}" == "$1" ]]; then
        exit 99
    fi
}

restore_prior_runtime() {
    local prior="$1"
    RESTORE_RUNTIME_ALIGNED=false
    if [[ -n "$prior" ]]; then
        log "Restoring prior release $prior"
        compose_cmd "$prior" up -d || return 1
        RESTORE_RUNTIME_ALIGNED=true
        wait_release "$prior" && wait_dragon_activation "$prior"
    else
        docker compose --project-directory "$ROOT" --env-file "$ROOT/.env" \
            --env-file "$2/release.env" \
            -f "$2/docker-compose.yml" down >/dev/null 2>&1 || return 1
        RESTORE_RUNTIME_ALIGNED=true
    fi
}

restore_prior_or_stop_candidate() {
    local prior="$1" candidate_dir="$2"
    if restore_prior_runtime "$prior" "$candidate_dir"; then
        return 0
    fi
    if [[ "$RESTORE_RUNTIME_ALIGNED" != true ]]; then
        warn "prior runtime could not start; stopping the candidate to preserve the current pointer"
        docker compose --project-directory "$ROOT" --env-file "$ROOT/.env" \
            --env-file "$candidate_dir/release.env" \
            -f "$candidate_dir/docker-compose.yml" stop >/dev/null 2>&1 \
            || warn "candidate stop also failed; runtime state needs operator repair"
    fi
    if ! set_hold runtime-restoration-failure; then
        warn "could not enable the update hold after runtime restoration failed"
        disable_update_cron || warn "could not remove automatic update cron"
    fi
    return 1
}

activate_manifest() {
    local manifest="$1" tools="$2" action="$3" prior="" candidate_dir stage reason
    local pointer_rc=0 observed_current=""
    local postcommit_ok=true postcommit_stage="" postcommit_reason=""
    local candidate_id="$MANIFEST_RELEASE_ID"
    prior="$(current_release_id 2>/dev/null || true)"
    if ! prepare_release "$manifest" "$tools" "$prior"; then
        json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" prepare \
            "candidate release staging failed" || warn "could not record update failure state"
        return 1
    fi
    candidate_dir="$RELEASES_DIR/$candidate_id"

    if [[ "$candidate_id" == "$prior" ]]; then
        if ! release_services_present "$candidate_id"; then
            log "Restarting missing current-release containers with the activation policy"
            if ! candidate_compose_cmd "$candidate_id" up -d; then
                json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" compose-start \
                    "current release containers could not be restarted" || true
                return 1
            fi
        fi
        if ! wait_release "$candidate_id"; then
            json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" health \
                "current release is unhealthy" || true
            return 1
        fi
        if ! wait_dragon_activation "$candidate_id"; then
            json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" activation \
                "current release did not reach activation readiness" || true
            return 1
        fi
        if ! prepare_host_dispatch "$candidate_id" "$tools"; then
            json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" host-tools \
                "host tool convergence failed" || warn "could not record update failure state"
            return 1
        fi
        commit_host_dispatch || { die "host tool activation failed"; return 1; }
        compose_cmd "$candidate_id" up -d || { die "restart-policy convergence failed"; return 1; }
        mark_release_accepted "$candidate_id" || { die "accepted-release evidence could not be recorded"; return 1; }
        clear_rejected "$candidate_id" \
            || warn "current release is healthy, but rejected-release state could not be cleared"
        json_state "$STATE_DIR/last-update.json" current "$action" "$candidate_id" current "already current" \
            || warn "could not record current release state"
        log "Release $candidate_id is already current"
        return 0
    fi

    if [[ -n "$prior" ]] && ! mark_release_accepted "$prior"; then
        die "cannot preserve accepted-release evidence for current release $prior"
        return 1
    fi

    log "Starting candidate release $candidate_id"
    if ! candidate_compose_cmd "$candidate_id" up -d; then
        stage=compose-start
        reason="candidate Compose start failed"
    elif ! wait_release "$candidate_id"; then
        stage=health
        reason="candidate health check failed"
    elif ! wait_dragon_activation "$candidate_id"; then
        stage=activation
        reason="candidate activation readiness failed"
    else
        stage=""
        reason=""
    fi

    if [[ -n "$stage" ]]; then
        if ! restore_prior_or_stop_candidate "$prior" "$candidate_dir"; then
            reason="$reason; prior release health restoration failed"
        fi
        reject_release "$candidate_id" "$stage" "$reason" || true
        json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" "$stage" "$reason" \
            || warn "could not record update failure state"
        die "$reason"
        return 1
    fi

    if ! record_release_parent "$candidate_id" "$prior"; then
        reason="rollback lineage commit failed"
        if ! restore_prior_or_stop_candidate "$prior" "$candidate_dir"; then
            reason="$reason; prior runtime restoration failed"
        fi
        reject_release "$candidate_id" lineage "rollback lineage commit failed" || true
        json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" lineage \
            "$reason" || warn "could not record update failure state"
        die "$reason"
        return 1
    fi
    maybe_power_loss before-current
    if ! prepare_host_dispatch "$candidate_id" "$tools"; then
        reason="host tool convergence failed"
        if ! restore_prior_or_stop_candidate "$prior" "$candidate_dir"; then
            reason="$reason; prior runtime restoration failed"
        fi
        reject_release "$candidate_id" host-tools "$reason" || true
        json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" host-tools \
            "$reason" || warn "could not record update failure state"
        die "$reason"
        return 1
    fi
    atomic_symlink "$RELEASES_DIR" current "$candidate_id" || pointer_rc=$?
    if [[ "$pointer_rc" -ne 0 ]]; then
        observed_current="$(current_release_id 2>/dev/null || true)"
    fi
    if [[ "$pointer_rc" -ne 0 && "$observed_current" != "$candidate_id" ]]; then
        reason="current pointer commit failed"
        if ! restore_prior_or_stop_candidate "$prior" "$candidate_dir"; then
            reason="$reason; prior runtime restoration failed"
        fi
        reject_release "$candidate_id" pointer "$reason" || true
        json_state "$STATE_DIR/last-update.json" failed "$action" "$candidate_id" pointer \
            "$reason" || warn "could not record update failure state"
        die "$reason"
        return 1
    fi
    if [[ "$pointer_rc" -ne 0 ]]; then
        postcommit_ok=false
        postcommit_stage=pointer-sync
        postcommit_reason="release pointer changed, but its directory sync failed"
        warn "$postcommit_reason"
    fi
    maybe_power_loss after-current
    if ! commit_host_dispatch; then
        if [[ "$postcommit_ok" == true ]]; then
            postcommit_ok=false
            postcommit_stage=host-tools
            postcommit_reason="release committed, but host service metadata refresh failed"
        fi
        warn "release committed, but host service metadata could not be refreshed"
    fi
    if ! compose_cmd "$candidate_id" up -d; then
        if [[ "$postcommit_ok" == true ]]; then
            postcommit_ok=false
            postcommit_stage=restart-policy
            postcommit_reason="release committed, but normal container restart policy could not be restored"
        fi
        warn "release committed, but normal container restart policy could not be restored"
    fi
    if ! mark_release_accepted "$candidate_id"; then
        if [[ "$postcommit_ok" == true ]]; then
            postcommit_ok=false
            postcommit_stage=accepted
            postcommit_reason="release committed, but accepted-release evidence could not be recorded"
        fi
        warn "release committed, but accepted-release evidence could not be recorded"
    fi
    if [[ "$postcommit_ok" == true ]]; then
        json_state "$STATE_DIR/last-update.json" success "$action" "$candidate_id" activated "" \
            || warn "release committed, but success state could not be recorded"
    else
        json_state "$STATE_DIR/last-update.json" degraded "$action" "$candidate_id" "$postcommit_stage" \
            "$postcommit_reason" \
            || warn "release committed, but degraded state could not be recorded"
    fi
    clear_rejected "$candidate_id" \
        || warn "release committed, but rejected-release state could not be cleared"
    log "Activated release $candidate_id"
    [[ "$postcommit_ok" == true ]] || return 2
}

download_candidate_manifest() {
    local output="$1" url
    local -a curl_protocols
    if [[ -n "${CLOUDNVR_CANDIDATE_MANIFEST:-}" ]]; then
        atomic_copy "$CLOUDNVR_CANDIDATE_MANIFEST" "$output" 0644
        return
    fi
    url="${BUNDLE_BASE_URL%/}/$CHANNEL/release.json"
    case "$url" in
        https://*) curl_protocols=(--proto '=https' --proto-redir '=https') ;;
        http://*)
            if [[ "${CLOUDNVR_ALLOW_INSECURE_BUNDLE:-0}" != 1 ]]; then
                die "release manifests require HTTPS: $url"
                return 1
            fi
            curl_protocols=(--proto '=http,https' --proto-redir '=http,https')
            ;;
        *) die "release manifest URL uses an unsupported protocol: $url"; return 1 ;;
    esac
    curl -fsSL "${curl_protocols[@]}" --connect-timeout 10 --max-time 60 \
        "$url" -o "$output"
}

pull_candidate_images() {
    log "Pulling candidate images while the current release stays online"
    docker pull "$MANIFEST_DRAGON_REF" || { die "Dragon image pull failed"; return 1; }
    docker pull "$MANIFEST_CONNECTOR_REF" || { die "connector image pull failed"; return 1; }
}

verify_candidate_images() {
    verify_image "$MANIFEST_DRAGON_REF" cloudnvr-dragon "$MANIFEST_SOURCE_REV" || return 1
    verify_image "$MANIFEST_CONNECTOR_REF" cloudnvr-connector "$MANIFEST_SOURCE_REV" "$MANIFEST_NVR_REV"
}

ensure_rollback_images() {
    if ! docker image inspect "$MANIFEST_DRAGON_REF" >/dev/null 2>&1; then
        docker pull "$MANIFEST_DRAGON_REF" || { die "Dragon rollback image is unavailable"; return 1; }
    fi
    if ! docker image inspect "$MANIFEST_CONNECTOR_REF" >/dev/null 2>&1; then
        docker pull "$MANIFEST_CONNECTOR_REF" || { die "connector rollback image is unavailable"; return 1; }
    fi
    verify_candidate_images
}

cleanup_staging() {
    local path
    shopt -s nullglob
    for path in "$RELEASES_DIR"/.staging-*; do
        [[ -d "$path" && ! -L "$path" ]] || continue
        rm -rf -- "$path"
    done
    shopt -u nullglob
}

prune_old_releases() {
    [[ "${CLOUDNVR_SKIP_PRUNE:-0}" != 1 ]] || return 0
    [[ "${CLOUDNVR_TEST_RELEASE_PRUNE_FAIL:-0}" != 1 ]] || return 1
    local current parent="" rejected=""
    current="$(current_release_id 2>/dev/null || true)"
    if [[ -n "$current" ]]; then
        parent="$(release_parent_id "$current" 2>/dev/null || true)"
    fi
    rejected="$(rejected_release_id 2>/dev/null || true)"
    python3 - "$RELEASES_DIR" "$current" "$parent" "$rejected" <<'PY' || return 1
import re, shutil, sys, time
from pathlib import Path
root, current, parent, rejected = Path(sys.argv[1]), sys.argv[2], sys.argv[3], sys.argv[4]
dirs = [p for p in root.iterdir()
        if p.is_dir() and not p.is_symlink() and re.fullmatch(r"[0-9a-f]{64}", p.name)]
dirs.sort(key=lambda p: p.stat().st_mtime, reverse=True)
cutoff = time.time() - 7 * 86400
for index, path in enumerate(dirs):
    if index < 2 or path.name in (current, parent, rejected) or path.stat().st_mtime >= cutoff:
        continue
    shutil.rmtree(path)
PY
    prune_product_images
}

prune_product_images() {
    local path ref id keep_ids="" image_ids image_id created old
    shopt -s nullglob
    for path in "$RELEASES_DIR"/[0-9a-f]*; do
        [[ -d "$path" && ! -L "$path" && "${path##*/}" =~ ^[0-9a-f]{64}$ ]] || continue
        while IFS= read -r ref; do
            [[ -n "$ref" ]] || continue
            while IFS= read -r image_id; do
                [[ -n "$image_id" ]] && keep_ids+="$image_id"$'\n'
            done < <(docker image inspect --format '{{.Id}}' "$ref" 2>/dev/null || true)
        done < <(python3 - "$path/release.json" <<'PY' 2>/dev/null || true
import json, sys
value=json.load(open(sys.argv[1], encoding="utf-8"))
print(value["images"]["dragon"]["reference"])
print(value["images"]["connector"]["reference"])
PY
)
    done
    shopt -u nullglob
    image_ids="$({
        docker image ls --no-trunc --filter label=org.opencontainers.image.title=cloudnvr-dragon --format '{{.ID}}' 2>/dev/null || true
        docker image ls --no-trunc --filter label=org.opencontainers.image.title=cloudnvr-connector --format '{{.ID}}' 2>/dev/null || true
    } | sort -u)"
    while IFS= read -r id; do
        [[ -n "$id" ]] || continue
        grep -qxF "$id" <<< "$keep_ids" && continue
        created="$(docker image inspect --format '{{.Created}}' "$id" 2>/dev/null || true)"
        [[ -n "$created" ]] || continue
        old="$(python3 - "$created" <<'PY' 2>/dev/null || printf no
from datetime import datetime, timezone
import sys
created=sys.argv[1].replace("Z", "+00:00")
try: value=datetime.fromisoformat(created)
except ValueError: print("no"); raise SystemExit
if value.tzinfo is None: value=value.replace(tzinfo=timezone.utc)
print("yes" if (datetime.now(timezone.utc)-value).total_seconds() > 7*86400 else "no")
PY
)"
        [[ "$old" == yes ]] || continue
        docker image rm "$id" >/dev/null 2>&1 || true
    done <<< "$image_ids"
}

cmd_update() (
    local force=false candidate_tmp tools_tmp rejected current
    [[ "${1:-}" == --force ]] && { force=true; shift; }
    [[ "$#" -eq 0 ]] || { die "usage: nvr update [--force]"; return 2; }
    require_root || return 1
    acquire_mutation_lock || {
        log "Another CloudNVR update owns the lock. Exiting."
        [[ "${CLOUDNVR_INSTALLING:-0}" != 1 ]] || return 1
        return 0
    }
    require_install || return 1
    load_host_env || return 1
    mkdir -p "$RELEASES_DIR" "$STATE_DIR" || return 1
    durable_sync_dirs "$RELEASES_DIR" "$STATE_DIR" || return 1
    if [[ -f "$STATE_DIR/update-hold" ]]; then
        log "Updates are held. Run 'nvr hold off' before updating."
        [[ "${CLOUDNVR_INSTALLING:-0}" != 1 ]] || return 1
        return 0
    fi
    cleanup_staging
    candidate_tmp="$(mktemp "${TMPDIR:-/tmp}/cloudnvr-release.json.XXXXXXXX")"
    tools_tmp="$(mktemp -d "${TMPDIR:-/tmp}/cloudnvr-tools.XXXXXXXX")"
    trap 'rm -f "${candidate_tmp:-}"; [[ -z "${tools_tmp:-}" ]] || rm -rf -- "$tools_tmp"' EXIT

    if ! download_candidate_manifest "$candidate_tmp"; then
        json_state "$STATE_DIR/last-update.json" failed update "" resolve "cannot resolve channel manifest"
        return 1
    fi
    if ! manifest_load "$candidate_tmp"; then
        json_state "$STATE_DIR/last-update.json" failed update "" manifest "invalid channel release manifest"
        return 1
    fi
    [[ "$MANIFEST_PLATFORM" == "$PLATFORM" ]] || { die "candidate platform mismatch"; return 1; }
    current="$(current_release_id 2>/dev/null || true)"
    if [[ "$MANIFEST_RELEASE_ID" == "$current" ]]; then
        log "Release $current is already current"
    fi
    rejected="$(rejected_release_id 2>/dev/null || true)"
    if [[ "$MANIFEST_RELEASE_ID" == "$rejected" \
       && "$MANIFEST_RELEASE_ID" != "$current" \
       && "$force" != true ]] \
       && ! release_was_committed "$MANIFEST_RELEASE_ID"; then
        log "Skipping rejected release $rejected. Use 'nvr update --force' to retry it."
        json_state "$STATE_DIR/last-update.json" skipped update "$rejected" rejected "candidate is marked rejected"
        return 0
    fi
    if ! pull_candidate_images; then
        json_state "$STATE_DIR/last-update.json" failed update "$MANIFEST_RELEASE_ID" pull "image pull failed"
        return 1
    fi
    if ! verify_candidate_images; then
        reject_release "$MANIFEST_RELEASE_ID" image "image labels or architecture failed validation" || true
        json_state "$STATE_DIR/last-update.json" failed update "$MANIFEST_RELEASE_ID" image \
            "image validation failed" || warn "could not record update failure state"
        return 1
    fi
    if ! extract_host_tools "$MANIFEST_DRAGON_REF" "$tools_tmp"; then
        reject_release "$MANIFEST_RELEASE_ID" host-tools "candidate host tools failed validation" || true
        json_state "$STATE_DIR/last-update.json" failed update "$MANIFEST_RELEASE_ID" host-tools \
            "candidate host tools failed validation" || warn "could not record update failure state"
        return 1
    fi
    write_release_env "$tools_tmp/release.env" "$MANIFEST_DRAGON_DIGEST" "$MANIFEST_CONNECTOR_DIGEST"
    if ! validate_compose "$tools_tmp" "$tools_tmp/release.env"; then
        reject_release "$MANIFEST_RELEASE_ID" compose "candidate Compose contract failed validation" || true
        json_state "$STATE_DIR/last-update.json" failed update "$MANIFEST_RELEASE_ID" compose \
            "candidate Compose contract failed validation" || warn "could not record update failure state"
        return 1
    fi
    if [[ "${CLOUDNVR_PREFLIGHT_ONLY:-0}" == 1 ]]; then
        log "Candidate $MANIFEST_RELEASE_ID passed host preflight"
        return 0
    fi
    activate_manifest "$candidate_tmp" "$tools_tmp" update || return $?
    prune_old_releases || warn "old release pruning failed"
)

cmd_rollback() (
    local current current_dir parent parent_dir tools_tmp hold_was_enabled=false restore_ok postcommit_ok=true
    local postcommit_stage="" postcommit_reason="" pointer_rc=0 observed_current=""
    require_root || return 1
    acquire_mutation_lock || return 1
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    current_dir="$RELEASES_DIR/$current"
    manifest_load "$current_dir/release.json" || return 1
    parent="$(release_parent_id "$current" 2>/dev/null || true)"
    release_id_is_valid "$parent" || { die "release $current has no rollback parent"; return 1; }
    parent_dir="$RELEASES_DIR/$parent"
    manifest_load "$parent_dir/release.json" || return 1
    ensure_rollback_images || return 1
    tools_tmp="$(mktemp -d "${TMPDIR:-/tmp}/cloudnvr-rollback-tools.XXXXXXXX")"
    trap 'rm -rf -- "${tools_tmp:-}"' EXIT
    extract_host_tools "$MANIFEST_DRAGON_REF" "$tools_tmp" || return 1
    prepare_release "$parent_dir/release.json" "$tools_tmp" "" \
        || { die "stored rollback release failed validation"; return 1; }
    [[ -f "$STATE_DIR/update-hold" ]] && hold_was_enabled=true
    if [[ "$hold_was_enabled" != true ]] && ! set_hold manual-rollback; then
        die "cannot enable the update hold for rollback"
        return 1
    fi
    log "Starting rollback release $parent"
    if ! candidate_compose_cmd "$parent" up -d || ! wait_release "$parent" \
        || ! wait_dragon_activation "$parent"; then
        warn "rollback candidate failed; restoring current release $current"
        restore_ok=true
        if ! restore_prior_or_stop_candidate "$current" "$parent_dir"; then
            restore_ok=false
            warn "current release restoration failed during rollback"
        fi
        if [[ "$hold_was_enabled" != true && "$restore_ok" == true ]]; then clear_hold || true; fi
        json_state "$STATE_DIR/last-update.json" failed rollback "$parent" health "rollback release was unhealthy"
        return 1
    fi
    prepare_host_dispatch "$parent" "$tools_tmp" || {
        restore_ok=true
        if ! restore_prior_or_stop_candidate "$current" "$parent_dir"; then
            restore_ok=false
            warn "current release restoration failed after host tool failure"
        fi
        if [[ "$hold_was_enabled" != true && "$restore_ok" == true ]]; then clear_hold || true; fi
        return 1
    }
    if ! record_release_parent "$parent" "$current"; then
        restore_ok=true
        if ! restore_prior_or_stop_candidate "$current" "$parent_dir"; then
            restore_ok=false
            warn "current release restoration failed after rollback lineage failure"
        fi
        if [[ "$hold_was_enabled" != true && "$restore_ok" == true ]]; then clear_hold || true; fi
        json_state "$STATE_DIR/last-update.json" failed rollback "$parent" lineage \
            "rollback lineage commit failed" || true
        return 1
    fi
    atomic_symlink "$RELEASES_DIR" current "$parent" || pointer_rc=$?
    if [[ "$pointer_rc" -ne 0 ]]; then
        observed_current="$(current_release_id 2>/dev/null || true)"
    fi
    if [[ "$pointer_rc" -ne 0 && "$observed_current" != "$parent" ]]; then
        restore_ok=true
        if ! restore_prior_or_stop_candidate "$current" "$parent_dir"; then
            restore_ok=false
            warn "current release restoration failed after pointer failure"
        fi
        if [[ "$hold_was_enabled" != true && "$restore_ok" == true ]]; then clear_hold || true; fi
        json_state "$STATE_DIR/last-update.json" failed rollback "$parent" pointer "rollback pointer commit failed"
        return 1
    fi
    if [[ "$pointer_rc" -ne 0 ]]; then
        postcommit_ok=false
        postcommit_stage=pointer-sync
        postcommit_reason="rollback pointer changed, but its directory sync failed"
        warn "$postcommit_reason"
    fi
    if ! commit_host_dispatch; then
        if [[ "$postcommit_ok" == true ]]; then
            postcommit_ok=false
            postcommit_stage=host-tools
            postcommit_reason="rollback committed, but host service metadata refresh failed"
        fi
        warn "rollback committed, but host service metadata could not be refreshed"
    fi
    if ! compose_cmd "$parent" up -d; then
        if [[ "$postcommit_ok" == true ]]; then
            postcommit_ok=false
            postcommit_stage=restart-policy
            postcommit_reason="rollback committed, but normal container restart policy could not be restored"
        fi
        warn "rollback committed, but normal container restart policy could not be restored"
    fi
    if ! mark_release_accepted "$parent"; then
        if [[ "$postcommit_ok" == true ]]; then
            postcommit_ok=false
            postcommit_stage=accepted
            postcommit_reason="rollback committed, but accepted-release evidence could not be recorded"
        fi
        warn "rollback committed, but accepted-release evidence could not be recorded"
    fi
    if [[ "$postcommit_ok" == true ]]; then
        json_state "$STATE_DIR/last-update.json" success rollback "$parent" activated "update hold enabled" \
            || warn "rollback committed, but success state could not be recorded"
    else
        json_state "$STATE_DIR/last-update.json" degraded rollback "$parent" "$postcommit_stage" \
            "$postcommit_reason" \
            || warn "rollback committed, but degraded state could not be recorded"
    fi
    log "Rolled back to $parent. Automatic updates are held."
    [[ "$postcommit_ok" == true ]] || return 2
)

cmd_start() {
    local service="${1:-}" current
    [[ -z "$service" || "$service" == dragon || "$service" == connector ]] \
        || { die "service must be dragon or connector"; return 2; }
    acquire_mutation_lock || return 1
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    if [[ -n "$service" ]]; then
        compose_cmd "$current" up -d "$service"
        wait_service "$current" "$service" "$([[ "$service" == dragon ]] && printf '%s' "$DRAGON_DEADLINE" || printf '%s' "$CONNECTOR_DEADLINE")"
    else
        compose_cmd "$current" up -d
        wait_release "$current"
    fi
}

cmd_stop() {
    local service="${1:-}" current
    [[ -z "$service" || "$service" == dragon || "$service" == connector ]] \
        || { die "service must be dragon or connector"; return 2; }
    acquire_mutation_lock || return 1
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    if [[ -n "$service" ]]; then
        compose_cmd "$current" stop "$service"
    else
        compose_cmd "$current" down
    fi
}

cmd_service_stop() {
    local current
    [[ "$#" -eq 0 ]] || { die "service-stop does not accept arguments"; return 2; }
    require_root || return 1
    acquire_service_lock stop || return 1
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    compose_cmd "$current" down
}

legacy_stop_locked() {
    local unit load_state expected actual active_rc rc=0 identity_dir="$STATE_DIR/legacy-units"
    for unit in dragon.service connector.service; do
        if ! load_state="$(systemctl show -p LoadState --value "$unit" 2>/dev/null)"; then
            die "cannot inspect legacy $unit"
            rc=1
            continue
        fi
        case "$load_state" in
            not-found) continue ;;
            loaded|masked) ;;
            *)
                die "legacy $unit has unsafe load state: $load_state"
                rc=1
                continue
                ;;
        esac
        if systemd_active_status "$unit"; then
            :
        else
            active_rc=$?
            if [[ "$active_rc" -eq 1 ]]; then
                continue
            fi
            die "cannot inspect legacy $unit active state"
            rc=1
            continue
        fi
        if [[ ! -f "$identity_dir/$unit.sha256" || -L "$identity_dir/$unit.sha256" ]]; then
            die "loaded $unit has no adopted CloudNVR identity"
            rc=1
            continue
        fi
        expected="$(cat "$identity_dir/$unit.sha256")"
        if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]] \
           || ! actual="$(systemctl cat "$unit" 2>/dev/null | sha256_file /dev/stdin)" \
           || [[ "$actual" != "$expected" ]]; then
            die "loaded $unit does not match its adopted identity"
            rc=1
            continue
        fi
        if ! systemctl stop "$unit"; then
            die "cannot stop legacy $unit"
            rc=1
            continue
        fi
        if ! systemd_inactive_verified "$unit"; then
            die "legacy $unit stop state could not be verified"
            rc=1
        fi
    done
    return "$rc"
}

cmd_legacy_stop() {
    [[ "$#" -eq 0 ]] || { die "legacy-stop does not accept arguments"; return 2; }
    require_root || return 1
    acquire_mutation_lock || return 1
    require_install || return 1
    legacy_stop_locked
}

cmd_service_start() {
    local current
    [[ "$#" -eq 0 ]] || { die "service-start does not accept arguments"; return 2; }
    require_root || return 1
    acquire_service_lock start || return 1
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    legacy_stop_locked || return 1
    compose_cmd "$current" up -d || return 1
    wait_release "$current" && wait_dragon_activation "$current"
}

cmd_restart() {
    local service="${1:-}" current
    [[ -z "$service" || "$service" == dragon || "$service" == connector ]] \
        || { die "service must be dragon or connector"; return 2; }
    acquire_mutation_lock || return 1
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    if [[ -n "$service" ]]; then
        compose_cmd "$current" up -d --force-recreate "$service"
        wait_service "$current" "$service" "$([[ "$service" == dragon ]] && printf '%s' "$DRAGON_DEADLINE" || printf '%s' "$CONNECTOR_DEADLINE")"
    else
        compose_cmd "$current" up -d --force-recreate
        wait_release "$current"
    fi
}

cmd_logs() {
    local service="" follow="" current arg
    for arg in "$@"; do
        case "$arg" in
            dragon|connector) [[ -z "$service" ]] || { die "choose one service"; return 2; }; service="$arg" ;;
            -f|--follow) follow=-f ;;
            *) die "usage: nvr logs [dragon|connector] [-f]"; return 2 ;;
        esac
    done
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    if [[ -n "$service" ]]; then
        compose_cmd "$current" logs ${follow:+"$follow"} "$service"
    else
        compose_cmd "$current" logs ${follow:+"$follow"}
    fi
}

cmd_channel() {
    local requested="${1:-}"
    if [[ -z "$requested" ]]; then
        require_install || return 1
        load_host_env || return 1
        printf '%s\n' "$CHANNEL"
        return
    fi
    [[ "$#" -eq 1 ]] || { die "usage: nvr channel [CHANNEL]"; return 2; }
    require_root || return 1
    channel_is_valid "$requested" || { die "invalid channel: $requested"; return 1; }
    acquire_mutation_lock || return 1
    require_install || return 1
    load_host_env || return 1
    python3 - "$ENV_FILE" "$requested" <<'PY' | atomic_write "$ENV_FILE" 0600
import sys
path, channel = sys.argv[1:]
lines = open(path, encoding="utf-8").read().splitlines()
seen = False
for line in lines:
    if line.startswith("CHANNEL="):
        print("CHANNEL=" + channel)
        seen = True
    else:
        print(line)
if not seen: print("CHANNEL=" + channel)
PY
    log "Channel set to $requested. Run 'nvr update' to activate it."
}

cmd_hold() {
    local action="${1:-status}"
    case "$action" in
        on)
            require_root || return 1
            acquire_mutation_lock || return 1
            require_install || return 1
            mkdir -p "$STATE_DIR" || return 1
            durable_sync_dirs "$STATE_DIR" || return 1
            set_hold operator
            log "Automatic updates are held."
            ;;
        off)
            require_root || return 1
            acquire_mutation_lock || return 1
            require_install || return 1
            mkdir -p "$STATE_DIR" || return 1
            durable_sync_dirs "$STATE_DIR" || return 1
            clear_hold
            log "Automatic updates are enabled."
            ;;
        status)
            require_install || return 1
            if [[ -f "$STATE_DIR/update-hold" ]]; then
                printf 'on\n'
            else
                printf 'off\n'
            fi
            ;;
        *) die "usage: nvr hold [on|off|status]"; return 2 ;;
    esac
}

cmd_status() {
    local current="" health release_file hold=off rejected=none last=none usage runtime gpu schedule
    local runtimes gpu_output
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id 2>/dev/null || true)"
    [[ -f "$STATE_DIR/update-hold" ]] && hold=on
    rejected="$(rejected_release_id 2>/dev/null || printf none)"
    [[ -n "$rejected" ]] || rejected=none
    if [[ -f "$STATE_DIR/last-update.json" ]]; then
        last="$(python3 -c 'import json,sys; v=json.load(open(sys.argv[1])); print("%s at %s" % (v.get("result","?"),v.get("timestamp","?")))' "$STATE_DIR/last-update.json" 2>/dev/null || printf invalid)"
    fi
    usage="$(df -P "$ROOT/data/video" 2>/dev/null | awk 'NR==2 {print $5 " used (" $4 " KiB free)"}' || true)"
    runtimes="$(docker info --format '{{json .Runtimes}}' 2>/dev/null || true)"
    if grep -q 'nvidia' <<< "$runtimes"; then runtime=available; else runtime=unavailable; fi
    gpu_output="$(command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L 2>/dev/null || true)"
    gpu="${gpu_output%%$'\n'*}"
    if [[ -f "$CRON_FILE" ]]; then
        schedule="$(awk '$1 ~ /^[0-9]+$/ && $2 == 2 {printf "daily at 02:%02d", $1; exit}' "$CRON_FILE")"
        schedule="${schedule:-installed but invalid}"
    else
        schedule="not installed"
    fi
    printf 'Product: cloudnvr\n'
    printf 'Host architecture: %s\n' "$(uname -m)"
    printf 'Channel: %s\n' "$CHANNEL"
    printf 'Active release: %s\n' "${current:-none}"
    if [[ -n "$current" ]]; then
        release_file="$RELEASES_DIR/$current/release.json"
        manifest_load "$release_file" || return 1
        printf 'Dragon image: %s\n' "$MANIFEST_DRAGON_REF"
        printf 'Connector image: %s\n' "$MANIFEST_CONNECTOR_REF"
        local service_label
        for service in dragon connector; do
            health="$(container_health "$current" "$service" || true)"
            [[ "$service" == dragon ]] && service_label=Dragon || service_label=Connector
            printf '%s health: %s\n' "$service_label" "$health"
        done
    fi
    printf 'Last update: %s\n' "$last"
    printf 'Rejected release: %s\n' "$rejected"
    printf 'Automatic update: %s, hold %s\n' "$schedule" "$hold"
    printf 'Video filesystem: %s\n' "${usage:-unavailable}"
    printf 'NVIDIA runtime: %s\n' "$runtime"
    printf 'GPU: %s\n' "${gpu:-not visible from host probe}"
}

cmd_version() {
    local current version channel
    require_install || return 1
    load_host_env || return 1
    current="$(current_release_id)" || { die "there is no current release"; return 1; }
    manifest_load "$RELEASES_DIR/$current/release.json" || return 1
    printf 'Release: %s\n' "$MANIFEST_RELEASE_ID"
    printf 'Dragon source: %s\n' "$MANIFEST_SOURCE_REV"
    printf 'Connector source: %s\n' "$MANIFEST_SOURCE_REV"
    printf 'Connector NVR source: %s\n' "$MANIFEST_NVR_REV"
    version="$(image_label "$MANIFEST_DRAGON_REF" org.opencontainers.image.version 2>/dev/null || true)"
    channel="$(image_label "$MANIFEST_DRAGON_REF" channel 2>/dev/null || true)"
    printf 'Dragon image: %s (version %s, build channel %s)\n' "$MANIFEST_DRAGON_REF" "${version:-unknown}" "${channel:-unknown}"
    version="$(image_label "$MANIFEST_CONNECTOR_REF" org.opencontainers.image.version 2>/dev/null || true)"
    channel="$(image_label "$MANIFEST_CONNECTOR_REF" channel 2>/dev/null || true)"
    printf 'Connector image: %s (version %s, build channel %s)\n' "$MANIFEST_CONNECTOR_REF" "${version:-unknown}" "${channel:-unknown}"
}

usage() {
    printf '%s\n' \
        'Usage: nvr COMMAND [arguments]' \
        '' \
        '  status' \
        '  logs [dragon|connector] [-f]' \
        '  update [--force]' \
        '  version' \
        '  channel [dev|dev-<suffix>|YYYY.MM.INCREMENT]' \
        '  start [dragon|connector]' \
        '  stop [dragon|connector]' \
        '  restart [dragon|connector]' \
        '  rollback' \
        '  hold [on|off|status]'
}

main() {
    local command="${1:-help}"
    [[ "$#" -eq 0 ]] || shift
    case "$command" in
        status) cmd_status "$@" ;;
        logs) cmd_logs "$@" ;;
        update) cmd_update "$@" ;;
        version) cmd_version "$@" ;;
        channel) cmd_channel "$@" ;;
        start) cmd_start "$@" ;;
        stop) cmd_stop "$@" ;;
        service-start) cmd_service_start "$@" ;;
        service-stop) cmd_service_stop "$@" ;;
        legacy-stop) cmd_legacy_stop "$@" ;;
        restart) cmd_restart "$@" ;;
        rollback) [[ "$#" -eq 0 ]] || { die "usage: nvr rollback"; return 2; }; cmd_rollback ;;
        hold) cmd_hold "$@" ;;
        help|-h|--help) usage ;;
        *) die "unknown command: $command"; usage >&2; return 2 ;;
    esac
}

main "$@"
