#!/bin/sh
set -eu

# Release tooling replaces these markers before publishing this script.
AGENT_VERSION="0.1.1"
HUB_URL="https://dockman.k1nq.space"
REQUIREMENTS_URL="https://git.k1nq.space/api/packages/k1nq/generic/dockman-agent/0.1.1/requirements.txt"

INSTALL_ROOT=/opt/dockman-agent
RELEASES_DIR=$INSTALL_ROOT/releases
CURRENT_LINK=$INSTALL_ROOT/current
COMMAND_LINK=/usr/local/bin/dockman-agent
CONFIG_DIR=/etc/dockman-agent
CONFIG_FILE=$CONFIG_DIR/config.toml
STATE_DIR=/var/lib/dockman-agent
LOG_DIR=/var/log/dockman-agent
RUNTIME_DIR=/run/dockman-agent
CONTROL_SOCKET=$RUNTIME_DIR/control.sock
UNIT_FILE=/etc/systemd/system/dockman-agent.service
LOCK_FILE=/run/lock/dockman-agent-maintenance.lock
PYTHON=

MODE=
REGISTRATION_TOKEN=
CONFIRM_AGENT_IDENTITY=
CONFIRM_PURGE=no
PREVIOUS_RELEASE=
NEW_RELEASE=
TOKEN_FILE=
SERVICE_WAS_ACTIVE=no
SERVICE_WAS_ENABLED=no
HAD_ENROLLMENT=no

usage() {
    cat <<'EOF'
Usage:
  install.sh install [--registration-token TOKEN]
  install.sh repair [--registration-token TOKEN]
  install.sh uninstall
  install.sh purge --confirm-agent-identity UUID --yes
EOF
}

fail() {
    printf '%s\n' "dockman-agent installer: $1" >&2
    exit 1
}

progress() {
    printf '{"kind":"installer_progress","phase":"%s","status":"%s"}\n' "$1" "$2"
}

cleanup() {
    if [ -n "$TOKEN_FILE" ]; then
        rm -f -- "$TOKEN_FILE"
    fi
}
trap cleanup EXIT HUP INT TERM

parse_arguments() {
    [ "$#" -gt 0 ] || { usage >&2; exit 2; }
    MODE=$1
    shift
    case "$MODE" in
        install|repair|uninstall|purge) ;;
        -h|--help) usage; exit 0 ;;
        *) usage >&2; exit 2 ;;
    esac
    while [ "$#" -gt 0 ]; do
        case "$1" in
            --registration-token)
                [ "$#" -ge 2 ] || fail "--registration-token requires a value"
                REGISTRATION_TOKEN=$2
                shift 2
                ;;
            --confirm-agent-identity)
                [ "$#" -ge 2 ] || fail "--confirm-agent-identity requires a value"
                CONFIRM_AGENT_IDENTITY=$2
                shift 2
                ;;
            --yes)
                CONFIRM_PURGE=yes
                shift
                ;;
            *) fail "unknown argument" ;;
        esac
    done
    if [ "$MODE" = purge ]; then
        [ "$CONFIRM_PURGE" = yes ] || fail "purge requires --yes"
        [ -n "$CONFIRM_AGENT_IDENTITY" ] || fail "purge requires --confirm-agent-identity"
    elif [ -n "$CONFIRM_AGENT_IDENTITY" ] || [ "$CONFIRM_PURGE" = yes ]; then
        fail "purge confirmation arguments are valid only with purge"
    fi
}

require_root_and_lock() {
    [ "$(id -u)" -eq 0 ] || fail "must run as root"
    command -v flock >/dev/null 2>&1 || fail "flock is required"
    mkdir -p /run/lock
    exec 9>"$LOCK_FILE"
    flock -n 9 || fail "another Agent maintenance operation is running"
}

validate_release_metadata() {
    case "$AGENT_VERSION$HUB_URL$REQUIREMENTS_URL" in
        *@@*) fail "installer release metadata was not rendered" ;;
    esac
    case "$HUB_URL" in https://*) ;; *) fail "embedded Hub URL must use HTTPS" ;; esac
    case "$REQUIREMENTS_URL" in https://*) ;; *) fail "embedded requirements URL must use HTTPS" ;; esac
}

detect_python() {
    for candidate in python3.14 python3.13 python3.12 python3; do
        if command -v "$candidate" >/dev/null 2>&1 \
            && "$candidate" -c 'import sys; raise SystemExit(0 if (3, 12) <= sys.version_info[:2] < (3, 15) else 1)' 2>/dev/null; then
            PYTHON=$candidate
            return 0
        fi
    done
    return 1
}

preflight() {
    progress preflight started
    detect_python || fail "Python >= 3.12 and < 3.15 is required"
    command -v curl >/dev/null 2>&1 || fail "curl is required"
    command -v systemctl >/dev/null 2>&1 || fail "systemd is required"
    command -v getent >/dev/null 2>&1 || fail "getent is required"
    [ -d /run/systemd/system ] || fail "systemd is not running"
    getent group docker >/dev/null 2>&1 || fail "Docker group does not exist"
    systemctl is-active --quiet docker.service || fail "Docker Engine is not active"
    progress preflight completed
}

ensure_system_identity() {
    progress system_identity started
    if getent group dockman >/dev/null 2>&1; then
        dockman_gid=$(getent group dockman | cut -d: -f3)
    else
        groupadd --system dockman
        dockman_gid=$(getent group dockman | cut -d: -f3)
    fi

    if getent passwd dockman >/dev/null 2>&1; then
        entry=$(getent passwd dockman)
        primary_gid=$(printf '%s' "$entry" | cut -d: -f4)
        home=$(printf '%s' "$entry" | cut -d: -f6)
        shell=$(printf '%s' "$entry" | cut -d: -f7)
        [ "$primary_gid" = "$dockman_gid" ] || fail "existing dockman user has incompatible primary group"
        [ "$home" = "$STATE_DIR" ] || fail "existing dockman user has incompatible home"
        [ "$shell" = /usr/sbin/nologin ] || fail "existing dockman user has incompatible shell"
    else
        useradd --system --gid dockman --groups docker --home-dir "$STATE_DIR" \
            --no-create-home --shell /usr/sbin/nologin dockman
    fi
    usermod -aG docker dockman
    progress system_identity completed
}

ensure_directories() {
    install -d -o root -g root -m 0755 "$INSTALL_ROOT" "$RELEASES_DIR"
    install -d -o root -g dockman -m 0750 "$CONFIG_DIR"
    install -d -o dockman -g dockman -m 0700 "$STATE_DIR" "$LOG_DIR"
}

validate_existing_state() {
    "$PYTHON" - "$STATE_DIR" "$HUB_URL" <<'PY'
import json
from pathlib import Path
import sys

state = Path(sys.argv[1])
hub_url = sys.argv[2].rstrip("/")
identity_path = state / "identity.json"
key_path = state / "agent_ed25519.key"
enrollment_path = state / "enrollment.json"
if identity_path.exists() != key_path.exists():
    raise SystemExit("incomplete Agent Identity state")
if enrollment_path.exists() and not identity_path.exists():
    raise SystemExit("enrollment exists without Agent Identity")
if enrollment_path.exists():
    identity = json.loads(identity_path.read_text())
    enrollment = json.loads(enrollment_path.read_text())
    if enrollment.get("agent_identity") != identity.get("agent_identity"):
        raise SystemExit("enrollment Agent Identity conflicts with local identity")
    if str(enrollment.get("hub_url", "")).rstrip("/") != hub_url:
        raise SystemExit("existing enrollment belongs to another Hub")
PY
}

toml_string() {
    "$PYTHON" -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$1"
}

write_config() {
    progress configuration started
    temp=$(mktemp "$CONFIG_DIR/.config.toml.XXXXXX")
    hub=$(toml_string "$HUB_URL")
    state=$(toml_string "$STATE_DIR")
    socket=$(toml_string "$CONTROL_SOCKET")
    version=$(toml_string "$AGENT_VERSION")
    cat >"$temp" <<EOF
[agent]
state_dir = $state
hub_url = $hub
hub_origin = $hub
docker_socket = "/var/run/docker.sock"
agent_version = $version
protocol_version = "1.0"
supported_protocol_versions = ["1.0"]
log_level = "INFO"

[control]
socket_path = $socket
EOF
    chown root:dockman "$temp"
    chmod 0640 "$temp"
    mv -f "$temp" "$CONFIG_FILE"
    progress configuration completed
}

write_unit() {
    progress systemd_unit started
    temp=$(mktemp /etc/systemd/system/.dockman-agent.service.XXXXXX)
    cat >"$temp" <<'EOF'
[Unit]
Description=Dockman Agent
Wants=network-online.target
After=network-online.target docker.service
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=simple
User=dockman
Group=dockman
SupplementaryGroups=docker
WorkingDirectory=/var/lib/dockman-agent
ExecStart=/opt/dockman-agent/current/venv/bin/dockman-agent run --config /etc/dockman-agent/config.toml
Restart=on-failure
RestartSec=5
RuntimeDirectory=dockman-agent
RuntimeDirectoryMode=0750
StateDirectory=dockman-agent
StateDirectoryMode=0700
LogsDirectory=dockman-agent
LogsDirectoryMode=0700
UMask=0077
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=
PrivateTmp=yes
PrivateDevices=yes
PrivateMounts=yes
ProtectSystem=strict
ProtectHome=yes
ProtectHostname=yes
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
RestrictRealtime=yes
RestrictNamespaces=yes
LockPersonality=yes
RemoveIPC=yes
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

[Install]
WantedBy=multi-user.target
EOF
    chown root:root "$temp"
    chmod 0644 "$temp"
    mv -f "$temp" "$UNIT_FILE"
    systemctl daemon-reload
    progress systemd_unit completed
}

install_release() {
    progress release_install started
    NEW_RELEASE=$RELEASES_DIR/$AGENT_VERSION
    if [ -L "$CURRENT_LINK" ]; then
        PREVIOUS_RELEASE=$(readlink -f "$CURRENT_LINK" || true)
    fi
    if [ -x "$NEW_RELEASE/venv/bin/dockman-agent" ]; then
        progress release_install completed
        return
    fi
    if [ -e "$NEW_RELEASE" ]; then
        failed=$RELEASES_DIR/.failed-$AGENT_VERSION-$(date +%s)
        mv "$NEW_RELEASE" "$failed"
    fi
    stage=$(mktemp -d "$RELEASES_DIR/.stage-$AGENT_VERSION.XXXXXX")
    requirements=$stage/requirements.txt
    curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
        "$REQUIREMENTS_URL" --output "$requirements"
    if ! "$PYTHON" -m venv "$stage/venv"; then
        fail "Python virtual environment creation failed; install venv/ensurepip support for $PYTHON"
    fi
    if ! "$stage/venv/bin/python" -m pip install --disable-pip-version-check \
        --require-hashes --only-binary=:all: --requirement "$requirements"; then
        fail "Agent dependency installation failed"
    fi
    rm -f "$requirements"
    chown -R root:root "$stage"
    chmod -R go-w "$stage"
    mv "$stage" "$NEW_RELEASE"
    progress release_install completed
}

switch_release() {
    progress release_activate started
    link=$INSTALL_ROOT/.current.$$
    ln -s "$NEW_RELEASE" "$link"
    mv -Tf "$link" "$CURRENT_LINK"
    ln -sfn "$CURRENT_LINK/venv/bin/dockman-agent" "$COMMAND_LINK"
    progress release_activate completed
}

run_as_agent() {
    runuser -u dockman -- "$@"
}

initialize_identity() {
    if [ ! -f "$STATE_DIR/identity.json" ] || [ ! -f "$STATE_DIR/agent_ed25519.key" ]; then
        progress identity_init started
        run_as_agent "$CURRENT_LINK/venv/bin/dockman-agent" \
            --config "$CONFIG_FILE" identity init >/dev/null
        progress identity_init completed
    fi
}

enroll_if_needed() {
    if [ -f "$STATE_DIR/enrollment.json" ] && [ -z "$REGISTRATION_TOKEN" ]; then
        return
    fi
    [ -n "$REGISTRATION_TOKEN" ] || fail "Agent is not enrolled; a registration token is required"
    progress enrollment started
    TOKEN_FILE=$(mktemp "$STATE_DIR/.registration-token.XXXXXX")
    printf '%s\n' "$REGISTRATION_TOKEN" >"$TOKEN_FILE"
    chown dockman:dockman "$TOKEN_FILE"
    chmod 0600 "$TOKEN_FILE"
    if ! run_as_agent "$CURRENT_LINK/venv/bin/dockman-agent" \
        --config "$CONFIG_FILE" enroll --token-file "$TOKEN_FILE" >/dev/null; then
        rm -f "$TOKEN_FILE"
        TOKEN_FILE=
        REGISTRATION_TOKEN=
        return 1
    fi
    rm -f "$TOKEN_FILE"
    TOKEN_FILE=
    REGISTRATION_TOKEN=
    progress enrollment completed
}

read_status() {
    "$PYTHON" - "$CONTROL_SOCKET" <<'PY'
import json
import socket
import sys

client = socket.socket(socket.AF_UNIX)
client.settimeout(2)
client.connect(sys.argv[1])
client.sendall(b'{"kind":"status"}\n')
response = b""
while not response.endswith(b"\n") and len(response) <= 4096:
    chunk = client.recv(4096)
    if not chunk:
        break
    response += chunk
payload = json.loads(response)
if payload.get("kind") != "status" or payload.get("status") != "ok":
    raise SystemExit(1)
print(json.dumps(payload, sort_keys=True, separators=(",", ":")))
PY
}

check_readiness() {
    progress readiness started
    systemctl enable dockman-agent.service >/dev/null
    systemctl restart dockman-agent.service
    count=0
    while [ "$count" -lt 30 ]; do
        if systemctl is-active --quiet dockman-agent.service; then
            status=$(read_status 2>/dev/null || true)
            if [ -n "$status" ]; then
                reported_version=$(printf '%s' "$status" | "$PYTHON" -c 'import json,sys; print(json.load(sys.stdin)["agent_version"])')
                [ "$reported_version" = "$AGENT_VERSION" ] || fail "Agent readiness returned an unexpected version"
                printf '%s\n' "$status"
                progress readiness completed
                return 0
            fi
        fi
        count=$((count + 1))
        sleep 1
    done
    return 1
}

rollback_release() {
    [ -n "$PREVIOUS_RELEASE" ] || return 0
    [ -x "$PREVIOUS_RELEASE/venv/bin/dockman-agent" ] || return 0
    progress rollback started
    failed=$RELEASES_DIR/.failed-$AGENT_VERSION-$(date +%s)
    if [ "$NEW_RELEASE" != "$PREVIOUS_RELEASE" ] && [ -d "$NEW_RELEASE" ]; then
        mv "$NEW_RELEASE" "$failed"
    fi
    link=$INSTALL_ROOT/.current.$$
    ln -s "$PREVIOUS_RELEASE" "$link"
    mv -Tf "$link" "$CURRENT_LINK"
    systemctl restart dockman-agent.service || true
    restore_repair_service_state
    progress rollback completed
}

preserve_failed_release() {
    [ -n "$NEW_RELEASE" ] || return 0
    [ -d "$NEW_RELEASE" ] || return 0
    failed=$RELEASES_DIR/.failed-$AGENT_VERSION-$(date +%s)
    rm -f "$CURRENT_LINK"
    mv "$NEW_RELEASE" "$failed"
    NEW_RELEASE=
}

capture_service_state() {
    systemctl is-active --quiet dockman-agent.service && SERVICE_WAS_ACTIVE=yes || true
    systemctl is-enabled --quiet dockman-agent.service && SERVICE_WAS_ENABLED=yes || true
    [ -f "$STATE_DIR/enrollment.json" ] && HAD_ENROLLMENT=yes || true
}

restore_repair_service_state() {
    [ "$MODE" = repair ] || return 0
    [ "$HAD_ENROLLMENT" = yes ] || return 0
    if [ "$SERVICE_WAS_ENABLED" = no ]; then
        systemctl disable dockman-agent.service >/dev/null
    fi
    if [ "$SERVICE_WAS_ACTIVE" = no ]; then
        systemctl stop dockman-agent.service
    fi
}

prune_releases() {
    current=$(readlink -f "$CURRENT_LINK")
    latest_failed=
    for candidate in "$RELEASES_DIR"/.failed-*; do
        [ -d "$candidate" ] || continue
        if [ -z "$latest_failed" ] || [ "$candidate" -nt "$latest_failed" ]; then
            latest_failed=$candidate
        fi
    done
    for candidate in "$RELEASES_DIR"/* "$RELEASES_DIR"/.failed-* "$RELEASES_DIR"/.stage-*; do
        [ -e "$candidate" ] || continue
        [ "$candidate" = "$current" ] && continue
        [ -n "$PREVIOUS_RELEASE" ] && [ "$candidate" = "$PREVIOUS_RELEASE" ] && continue
        [ -n "$latest_failed" ] && [ "$candidate" = "$latest_failed" ] && continue
        rm -rf -- "$candidate"
    done
}

install_or_repair() {
    validate_release_metadata
    preflight
    ensure_system_identity
    ensure_directories
    validate_existing_state
    capture_service_state
    install_release
    systemctl stop dockman-agent.service >/dev/null 2>&1 || true
    write_config
    write_unit
    switch_release
    initialize_identity
    if ! enroll_if_needed; then
        systemctl disable --now dockman-agent.service >/dev/null 2>&1 || true
        fail "Agent enrollment failed; installed state and Agent Identity were preserved"
    fi
    if ! check_readiness; then
        if [ -n "$PREVIOUS_RELEASE" ]; then
            rollback_release
        else
            systemctl disable --now dockman-agent.service >/dev/null 2>&1 || true
            preserve_failed_release
        fi
        fail "Agent readiness failed"
    fi
    restore_repair_service_state
    prune_releases
}

uninstall_agent() {
    progress uninstall started
    systemctl disable --now dockman-agent.service >/dev/null 2>&1 || true
    rm -f "$UNIT_FILE" "$COMMAND_LINK" "$CONTROL_SOCKET"
    rm -rf "$INSTALL_ROOT" "$RUNTIME_DIR"
    systemctl daemon-reload
    progress uninstall completed
}

purge_agent() {
    [ -x "$CURRENT_LINK/venv/bin/dockman-agent" ] || fail "Agent is not installed"
    actual=$(run_as_agent "$CURRENT_LINK/venv/bin/dockman-agent" \
        --config "$CONFIG_FILE" identity show | "$CURRENT_LINK/venv/bin/python" -c 'import json,sys; print(json.load(sys.stdin)["agent_identity"])')
    [ "$actual" = "$CONFIRM_AGENT_IDENTITY" ] || fail "confirmed Agent Identity does not match installed state"
    progress enrollment_revoke started
    if run_as_agent "$CURRENT_LINK/venv/bin/dockman-agent" \
        --config "$CONFIG_FILE" enrollment revoke --reason local_purge >/dev/null 2>&1; then
        progress enrollment_revoke completed
    else
        progress enrollment_revoke unconfirmed
        printf '%s\n' "dockman-agent installer: Hub revocation was not confirmed; owner must revoke stale enrollment" >&2
    fi
    uninstall_agent
    rm -rf "$CONFIG_DIR" "$STATE_DIR" "$LOG_DIR"
    progress purge completed
}

main() {
    parse_arguments "$@"
    require_root_and_lock
    case "$MODE" in
        install|repair) install_or_repair ;;
        uninstall) uninstall_agent ;;
        purge) purge_agent ;;
    esac
}

main "$@"
