#!/bin/sh
# One-time operator bootstrap installer for an existing Pico installation.
#
# tools/publish-release.py generates this file from the release manifest it
# publishes. The origin and CLI digest below are that manifest's own immutable
# pins, never operator input, and there is no source override. The installer
# updates an existing /usr/local/bin/picoctl through the shipped upgrader; it
# does not bootstrap a fresh host, a pool, or a fleet, and it keeps no backups.
set -eu

base_url='https://git.a73x.sh/api/packages/a73x/generic/pico/0.1.0-network-attachments.dev.20261107T1845Z.ci.92f7099fa971.39d3c96fd9a7'
cli_sha256='c97dddda86a9863db2340bd49778436b681f228f0ee64abacdbcc3dc4014afd6'
stage_parent=${TMPDIR-/var/tmp}
installed_cli=/usr/local/bin/picoctl
cli_name=picoctl
manifest_name=release.json
max_download_seconds=1800
max_probe_seconds=10

usage() {
    printf '%s\n' 'Usage: bootstrap-operator.sh [--json]' >&2
    exit 2
}

fail() {
    printf 'bootstrap: %s\n' "$1" >&2
    exit 1
}

note() {
    printf 'bootstrap: %s\n' "$1" >&2
}

json=0
for argument in "$@"; do
    case $argument in
        --json) json=1 ;;
        *) usage ;;
    esac
done

stage=
cleanup() {
    status=$?
    trap - EXIT HUP INT TERM
    if [ -n "$stage" ]; then
        rm -rf -- "$stage"
    fi
    exit "$status"
}
trap cleanup EXIT
trap 'exit 130' HUP INT TERM

if [ "$(id -u)" != 0 ]; then
    fail 'a bootstrap update requires root; run it with sudo'
fi
if [ "$(uname -s)" != Linux ]; then
    fail 'this installer supports Linux hosts only'
fi
for tool in curl sha256sum python3 timeout stat awk mktemp; do
    command -v "$tool" >/dev/null 2>&1 || fail "required tool is missing: $tool"
done
if [ ! -f "$installed_cli" ] || [ ! -x "$installed_cli" ]; then
    fail "no installed picoctl at $installed_cli; this one-time installer updates an existing installation"
fi
python3 - "$installed_cli" <<'PYTHON'
import stat
import sys
from pathlib import Path

installed = Path(sys.argv[1])
for path in (installed, *installed.parents):
    metadata = path.lstat()
    trusted_type = (
        stat.S_ISREG(metadata.st_mode) and metadata.st_nlink == 1 and metadata.st_mode & 0o111
        if path == installed else stat.S_ISDIR(metadata.st_mode)
    )
    if metadata.st_uid != 0 or metadata.st_mode & 0o022 or not trusted_type:
        sys.exit(f"bootstrap: unsafe installed CLI path: {path}; requires protected root-owned files and directories")
PYTHON

case $base_url in
    https://?*) ;;
    *) fail 'the pinned release origin must use HTTPS' ;;
esac
case $cli_sha256 in
    '' | *[!0-9a-f]*) fail 'the pinned CLI digest is not a lowercase SHA-256' ;;
esac
if [ "${#cli_sha256}" -ne 64 ]; then
    fail 'the pinned CLI digest is not a lowercase SHA-256'
fi

# Environment chooses only the path, never a weaker ownership policy. Validate
# before fetching anything: runtime archives may be several gigabytes.
python3 - "$stage_parent" <<'PYTHON'
import os
import stat
import sys
from pathlib import Path

parent = Path(sys.argv[1])
if not parent.is_absolute() or ".." in parent.parts:
    sys.exit("bootstrap: staging TMPDIR must be an absolute path without parent traversal")
for path in (*reversed(parent.parents), parent):
    try:
        metadata = path.lstat()
    except OSError as error:
        sys.exit(f"bootstrap: cannot inspect staging parent {path}: {error}")
    if (not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != 0
            or (metadata.st_mode & 0o022 and not metadata.st_mode & stat.S_ISVTX)):
        sys.exit(f"bootstrap: unsafe staging parent {path}; TMPDIR requires real root-owned directories protected from replacement (shared writable parents must be sticky)")
if os.statvfs(parent).f_flag & os.ST_NOEXEC:
    sys.exit(f"bootstrap: staging {parent} is on a noexec filesystem; set TMPDIR to a protected root-owned directory on an executable filesystem")
PYTHON
stage=$(mktemp -d "$stage_parent/pico-bootstrap.XXXXXXXX") \
    || fail "cannot create a private stage under $stage_parent"
chmod 700 -- "$stage"

fetch() {
    fetch_url=$1
    fetch_output=$2
    # Bounded HTTPS-only transfer; redirects may not downgrade the scheme.
    timeout "$max_download_seconds" curl --fail --silent --show-error --location \
        --proto '=https' --proto-redir '=https' --tlsv1.2 --connect-timeout 20 \
        --max-time "$max_download_seconds" --output "$fetch_output" "$fetch_url" \
        || fail "download failed: $fetch_url"
}

note "downloading the pinned release manifest and candidate picoctl"
fetch "${base_url%/}/$manifest_name" "$stage/$manifest_name"
fetch "${base_url%/}/$cli_name" "$stage/$cli_name"
downloaded_sha256=$(sha256sum -- "$stage/$cli_name" | awk '{ print $1 }')
if [ "$downloaded_sha256" != "$cli_sha256" ]; then
    fail "downloaded CLI SHA-256 $downloaded_sha256 does not match the pinned digest"
fi
# The verified candidate stays non-writable; only the pure probe runs before
# the shipped upgrader owns every install, activation and state guard.
chmod 500 -- "$stage/$cli_name"

# The manifest must be the one this installer pins, before anything executes.
python3 - "$stage/$manifest_name" "$base_url" "$cli_sha256" <<'PYTHON'
import json
import sys

path, base_url, cli_sha256 = sys.argv[1:]
try:
    with open(path, "rb") as stream:
        manifest = json.load(stream)
except (OSError, ValueError) as error:
    sys.exit(f"bootstrap: cannot read the downloaded release manifest: {error}")
if not isinstance(manifest, dict):
    sys.exit("bootstrap: the downloaded release manifest is not a JSON object")
if manifest.get("base_url") != base_url:
    sys.exit("bootstrap: the release manifest names a different origin than this installer pins")
cli = manifest.get("cli")
if not isinstance(cli, dict) or cli.get("file") != "picoctl":
    sys.exit("bootstrap: the release manifest does not pin a picoctl CLI")
if cli.get("sha256") != cli_sha256:
    sys.exit("bootstrap: the release manifest pins a different CLI digest than this installer downloaded")
PYTHON

timeout "$max_probe_seconds" "$stage/$cli_name" upgrade --capabilities --json \
    >"$stage/candidate-capabilities.json" \
    || fail 'the staged candidate did not declare upgrade capabilities'
timeout "$max_probe_seconds" "$installed_cli" upgrade --capabilities --json \
    >"$stage/installed-capabilities.json" \
    || fail "the installed picoctl did not declare upgrade capabilities; refusing to update $installed_cli"

# Both declarations must agree with each other and with the pinned manifest, so
# a platform or compatibility change is rejected before any installed effect.
python3 - "$stage/$manifest_name" "$stage/candidate-capabilities.json" \
    "$stage/installed-capabilities.json" <<'PYTHON'
import json
import sys

manifest_path, candidate_path, installed_path = sys.argv[1:]


def refusal(message):
    sys.exit(f"bootstrap: {message}")


def load(path, label):
    try:
        with open(path, "rb") as stream:
            return json.load(stream)
    except (OSError, ValueError) as error:
        refusal(f"cannot read {label}: {error}")


manifest = load(manifest_path, "the downloaded release manifest")
candidate = load(candidate_path, "the staged candidate declaration")
installed = load(installed_path, "the installed picoctl declaration")
if candidate != installed:
    refusal("the staged candidate and the installed picoctl declare different upgrade capabilities")
for field in ("platform", "architecture", "compatibility"):
    if field not in manifest:
        refusal(f"the release manifest does not declare upgrade {field}")
    if candidate.get(field) != manifest[field]:
        refusal(
            f"declared upgrade {field} {candidate.get(field)!r} does not match "
            f"the release manifest {manifest[field]!r}"
        )
PYTHON

# The verified candidate owns CLI replacement, activation and every existing
# installer/guest/state guard. This shell waits for it so the EXIT trap can
# still remove the private stage, and it keeps no backup of the old CLI.
note "handing the pinned release manifest to the candidate upgrader"
if [ "$json" = 1 ]; then
    "$stage/$cli_name" upgrade --manifest "$stage/$manifest_name" --restart --json
else
    "$stage/$cli_name" upgrade --manifest "$stage/$manifest_name" --restart
fi
