#!/usr/bin/env python3
"""Restore missing Nookins update settings, then check the latest signed alpha.

Requires Python 3.10+. Supported pre-0.47 alphas are upgraded through the embedded
0.47 bridge, which can stop/restart the service and preserves the previous home.
On 0.47+, missing settings and optional config directories are repaired, then
the signed feed is checked. A feed check does not qualify update installation.
Existing trust, application data and recovery records are preserved.
"""
import argparse
from contextlib import contextmanager
import fcntl
import hashlib
import json
import os
from pathlib import Path
import re
import shlex
import stat
import subprocess
import sys
import tempfile
from urllib.request import HTTPRedirectHandler, Request, build_opener

ORIGIN = 'https://nookins.app'
ROOT_SHA256 = '495be108a7d0c0faa602cd68c553bb6b7ba91e9ded16d1974dbfe48ab2e3526e'
REPOSITORY = b'{"schema":1,"transport":"https","metadata":"https://nookins.app/updates/metadata/","targets":"https://nookins.app/updates/targets/"}\n'
LIMIT = 4 * 1024 * 1024


# BEGIN EMBEDDED BRIDGE
# Generated by tools/embed-repair-bridge.py from the unchanged public bridge.
BRIDGE_SHA256 = '8b994a664a7c0d6f0d765715024c624d4fdf5378e579f886f0549bfed5e29f52'
BRIDGE_TARGET = "0.47.0-alpha.2"
BRIDGE_SOURCES = frozenset([
    '0.40.0-alpha.1',
    '0.40.1-alpha.1',
    '0.41.0-alpha.1',
    '0.41.1-alpha.1',
    '0.41.2-alpha.1',
    '0.42.0-alpha.1',
    '0.42.1-alpha.1',
    '0.42.2-alpha.1',
    '0.42.3-alpha.1',
    '0.42.4-alpha.1',
    '0.43.0-alpha.1',
    '0.43.1-alpha.1',
    '0.43.2-alpha.1',
    '0.43.3-alpha.1',
    '0.43.4-alpha.1',
    '0.43.5-alpha.1',
    '0.43.6-alpha.1',
    '0.43.7-alpha.1',
    '0.43.8-alpha.1',
    '0.43.9-alpha.1',
    '0.44.0-alpha.1',
    '0.44.1-alpha.1',
    '0.44.2-alpha.1',
    '0.44.3-alpha.1',
    '0.45.0-alpha.1',
    '0.46.0-alpha.1',
])
BRIDGE_SCRIPT = r"""#!/usr/bin/env bash
# One-time upgrade bridge to Nookins 0.47.0-alpha.2.
#
# Existing installations run an older binary whose own updater cannot complete
# the schema-crossing transition to this version. This script downloads and
# verifies the 0.47.0-alpha.2 bundle, invokes the TARGET binary's migration
# worker, assembles a fresh home beside the source, then atomically swaps it in.
# The original home remains at .pre-0.47. Every phase is checkpointed in a
# stable private recovery directory; rerunning this same script resumes safely.
set -euo pipefail
umask 077

die() { printf 'update-to-0.47: %s\n' "$*" >&2; exit 1; }

HOME_DIR="${NOOKINS_HOME:-$HOME/.nookins}"
if [[ ${1:-} == --help ]]; then sed -n '2,18p' "$0"; exit 0; fi
if [[ $# -eq 2 && $1 == --home ]]; then HOME_DIR="$2"; shift 2; fi
[[ $# -eq 0 ]] || die "unexpected arguments; see --help"
[[ "$HOME_DIR" = /* ]] || die "home must be an absolute path"
case "$HOME_DIR" in /|*/|*/../*|*/./*|*/..|*/.|*//*) die "choose a normalized absolute home path" ;; esac
[[ "$(uname -s)" == Linux && "$(uname -m)" == x86_64 ]] || die "this alpha supports Linux x86_64"
for tool in curl sha256sum tar systemctl; do command -v "$tool" >/dev/null || die "missing required tool: $tool"; done

HOME_PARENT="${HOME_DIR%/*}"
HOME_NAME="${HOME_DIR##*/}"
WORK="${HOME_PARENT}/.${HOME_NAME#.}.bridge-recovery-0.47"
FRESH="${HOME_DIR}.bridge-next"
BACKUP="${HOME_DIR}.pre-0.47"
mkdir -p "$WORK"
chmod 700 "$WORK"

checkpoint() {
  local name="$1"
  : > "$WORK/.${name}.tmp"
  mv "$WORK/.${name}.tmp" "$WORK/$name"
  # Qualification-only interruption hook. It can only pause this process; it
  # grants no authority and changes no validation or recovery behavior.
  if [[ ${NOOKINS_BRIDGE_QUALIFICATION_INTERRUPT_AFTER:-} == "$name" ]]; then
    kill -STOP "$$"
  fi
}

record_once() {
  local path="$1" value="$2"
  if [[ -f "$path" ]]; then
    [[ "$(cat "$path")" == "$value" ]] || die "recovery state does not match this home"
  else
    printf '%s\n' "$value" > "$path"
  fi
}

record_once "$WORK/home" "$HOME_DIR"

# Recover checkpoint markers if interruption happened after a rename but before
# the marker write. The filesystem shape is authoritative and unambiguous.
if [[ -d "$BACKUP" && ! -e "$HOME_DIR" && -d "$FRESH" && -f "$WORK/prepared" ]]; then
  checkpoint source-moved
fi
if [[ -d "$BACKUP" && -x "$HOME_DIR/bin/nookins" && -f "$WORK/prepared" ]]; then
  current="$($HOME_DIR/bin/nookins --version 2>/dev/null | awk '{print $2}')"
  if [[ "$current" == "0.47.0-alpha.2" ]]; then
    checkpoint source-moved
    checkpoint target-moved
  fi
fi

if [[ -f "$WORK/source-version" ]]; then
  FROM_VERSION="$(cat "$WORK/source-version")"
else
  [[ -x "$HOME_DIR/bin/nookins" ]] || die "$HOME_DIR is not an installed Nookins home"
  FROM_VERSION="$($HOME_DIR/bin/nookins --version 2>/dev/null | awk '{print $2}')"
  if [[ "$FROM_VERSION" == "0.47.0-alpha.2" ]]; then
    printf 'update-to-0.47: already on 0.47.0-alpha.2; nothing to do.\n'
    exit 0
  fi
  case "$FROM_VERSION" in
    0.40.0-alpha.1|0.40.1-alpha.1|0.41.0-alpha.1|0.41.1-alpha.1|0.41.2-alpha.1|\
    0.42.0-alpha.1|0.42.1-alpha.1|0.42.2-alpha.1|0.42.3-alpha.1|0.42.4-alpha.1|\
    0.43.0-alpha.1|0.43.1-alpha.1|0.43.2-alpha.1|0.43.3-alpha.1|0.43.4-alpha.1|\
    0.43.5-alpha.1|0.43.6-alpha.1|0.43.7-alpha.1|0.43.8-alpha.1|0.43.9-alpha.1|\
    0.44.0-alpha.1|0.44.1-alpha.1|0.44.2-alpha.1|0.44.3-alpha.1|\
    0.45.0-alpha.1|0.46.0-alpha.1) ;;
    *) die "installed version $FROM_VERSION is not a supported 0.47 bridge source; update to a supported public alpha first" ;;
  esac
  printf '%s\n' "$FROM_VERSION" > "$WORK/source-version"
fi

ARCHIVE="nookins-0.47.0-alpha.2-linux-x86_64.tar.gz"
ARCHIVE_PATH="$WORK/$ARCHIVE"
ROOT_PATH="$WORK/root.json"
if ! printf '%s  %s\n' '7f34b599479fee262a8656874559e0d8eb1ab854d70bf1f19e056967326596d4' "$ARCHIVE_PATH" | sha256sum --check --status 2>/dev/null; then
  rm -f "$ARCHIVE_PATH"
  curl --fail --silent --show-error --proto '=https' --tlsv1.2 \
    --connect-timeout 15 --max-time 300 --max-filesize 536870912 \
    "https://nookins.app/downloads/$ARCHIVE" --output "$ARCHIVE_PATH"
fi
printf '%s  %s\n' '7f34b599479fee262a8656874559e0d8eb1ab854d70bf1f19e056967326596d4' "$ARCHIVE_PATH" | sha256sum --check --status \
  || die "downloaded bundle failed its pinned SHA-256 check"
if ! printf '%s  %s\n' '495be108a7d0c0faa602cd68c553bb6b7ba91e9ded16d1974dbfe48ab2e3526e' "$ROOT_PATH" | sha256sum --check --status 2>/dev/null; then
  rm -f "$ROOT_PATH"
  curl --fail --silent --show-error --proto '=https' --tlsv1.2 \
    --connect-timeout 15 --max-time 60 --max-filesize 8388608 \
    https://nookins.app/updates/root.json --output "$ROOT_PATH"
fi
printf '%s  %s\n' '495be108a7d0c0faa602cd68c553bb6b7ba91e9ded16d1974dbfe48ab2e3526e' "$ROOT_PATH" | sha256sum --check --status \
  || die "downloaded root metadata failed its pinned SHA-256 check"
checkpoint downloads-verified

GEN="${HOME_PARENT}/.${HOME_NAME#.}.bridge-generation-0.47"
if [[ ! -x "$GEN/bin/nookins" || ! -f "$GEN/manifest.json" ]]; then
  if [[ -e "$GEN" ]]; then chmod -R u+w "$GEN" 2>/dev/null || true; rm -rf "$GEN"; fi
  EXTRACTED="$WORK/generation-v1"
  if [[ -e "$EXTRACTED" ]]; then chmod -R u+w "$EXTRACTED" 2>/dev/null || true; rm -rf "$EXTRACTED"; fi
  tar --extract --gzip --file "$ARCHIVE_PATH" --directory "$WORK" --no-same-owner --same-permissions -- generation-v1
  mv "$EXTRACTED" "$GEN"
  chmod 700 "$GEN"
fi
GEN_BINARY="$GEN/bin/nookins"
TARGET_BIN="${HOME_PARENT}/.${HOME_NAME#.}.bridge-target-nookins-0.47"
GEN_BINARY_SHA="$(sha256sum "$GEN_BINARY" | awk '{print $1}')"
if [[ ! -x "$TARGET_BIN" || "$(sha256sum "$TARGET_BIN" 2>/dev/null | awk '{print $1}')" != "$GEN_BINARY_SHA" ]]; then
  rm -f "$TARGET_BIN"
  cp "$GEN_BINARY" "$TARGET_BIN"
  chmod 700 "$TARGET_BIN"
fi
[[ "$(sha256sum "$TARGET_BIN" | awk '{print $1}')" == "$GEN_BINARY_SHA" ]] || die "standalone target verifier copy changed"
TARGET_VERSION="$($TARGET_BIN --version 2>/dev/null | awk '{print $2}')"
[[ "$TARGET_VERSION" == "0.47.0-alpha.2" ]] || die "downloaded target is $TARGET_VERSION, expected 0.47.0-alpha.2"
MANIFEST_SHA="$(sha256sum "$GEN/manifest.json" | awk '{print $1}')"

if [[ ! -f "$WORK/prepared" ]]; then
  if [[ -e "$BACKUP" || -L "$BACKUP" ]]; then
    die "refusing existing backup path: $BACKUP; complete or restore the recorded bridge first"
  fi
  if [[ -e "$FRESH" || -L "$FRESH" ]]; then
    chmod -R u+w "$FRESH" 2>/dev/null || true
    rm -rf "$FRESH"
  fi
  "$TARGET_BIN" --home "$FRESH" maintenance release install-generation "$GEN" --manifest-sha256 "$MANIFEST_SHA" >/dev/null \
    || die "could not install the fresh target home; nothing was changed"

  DB_KEY="$(sed -n 's/^NOOKINS_DB_KEY=//p' "$HOME_DIR/secrets.env" 2>/dev/null | head -1 || true)"
  printf 'update-to-0.47: migrating config, database, and durable data (%s -> 0.47.0-alpha.2)…\n' "$FROM_VERSION"
  if [[ -n "$DB_KEY" ]]; then export NOOKINS_DB_KEY="$DB_KEY"; fi
  if ! "$TARGET_BIN" internal bridge-migrate --source-home "$HOME_DIR" --target-home "$FRESH" --staging "$WORK/staging"; then
    unset NOOKINS_DB_KEY || true
    chmod -R u+w "$FRESH" 2>/dev/null || true
    rm -rf "$FRESH"
    die "migration into the fresh home failed; your installation was not changed"
  fi
  unset NOOKINS_DB_KEY || true
  checkpoint prepared
else
  [[ -d "$FRESH" || -f "$WORK/target-moved" ]] || die "prepared target is missing from recovery state"
fi

if [[ ! -f "$WORK/service-was-active" ]]; then
  if systemctl --user is-active nookins.service >/dev/null 2>&1; then
    printf '1\n' > "$WORK/service-was-active"
  else
    printf '0\n' > "$WORK/service-was-active"
  fi
fi
SERVICE_WAS_ACTIVE="$(cat "$WORK/service-was-active")"
[[ "$SERVICE_WAS_ACTIVE" == 0 || "$SERVICE_WAS_ACTIVE" == 1 ]] || die "invalid recovery service state"

rollback() {
  local reason="$1"
  systemctl --user stop nookins.service >/dev/null 2>&1 || true
  if [[ -e "$HOME_DIR" || -L "$HOME_DIR" ]]; then
    chmod -R u+w "$HOME_DIR" 2>/dev/null || true
    rm -rf "$HOME_DIR"
  fi
  if [[ -d "$BACKUP" ]]; then mv "$BACKUP" "$HOME_DIR"; fi
  if [[ "$SERVICE_WAS_ACTIVE" == 1 ]]; then systemctl --user start nookins.service >/dev/null 2>&1 || true; fi
  rm -f "$WORK/source-moved" "$WORK/target-moved" "$WORK/service-started" "$WORK/ready" "$WORK/prepared"
  if [[ -e "$FRESH" || -L "$FRESH" ]]; then chmod -R u+w "$FRESH" 2>/dev/null || true; rm -rf "$FRESH"; fi
  die "$reason — rolled back to $FROM_VERSION"
}

if [[ ! -f "$WORK/source-moved" ]]; then
  [[ -d "$HOME_DIR" ]] || die "source home disappeared before the atomic swap"
  if [[ "$SERVICE_WAS_ACTIVE" == 1 ]]; then
    printf 'update-to-0.47: stopping the running service…\n'
    systemctl --user stop nookins.service || die "could not stop the service; nothing was swapped"
  fi
  mv "$HOME_DIR" "$BACKUP" || die "could not move the current home aside; nothing was swapped"
  checkpoint source-moved
fi

if [[ ! -f "$WORK/target-moved" ]]; then
  [[ ! -e "$HOME_DIR" && -d "$FRESH" && -d "$BACKUP" ]] || rollback "invalid swap recovery state"
  mv "$FRESH" "$HOME_DIR" || rollback "could not move the new home into place"
  checkpoint target-moved
fi

if [[ "$SERVICE_WAS_ACTIVE" == 1 ]]; then
  if [[ ! -f "$WORK/service-started" ]]; then
    printf 'update-to-0.47: starting 0.47.0-alpha.2…\n'
    systemctl --user start nookins.service || rollback "the new version did not start"
    checkpoint service-started
  fi
  deadline=$(( $(date +%s) + 120 ))
  until "$HOME_DIR/bin/nookins" --home "$HOME_DIR" service status >/dev/null 2>&1; do
    [[ $(date +%s) -ge $deadline ]] && rollback "the new version did not become ready"
    sleep 2
  done
fi
checkpoint ready

NEW_VERSION="$($HOME_DIR/bin/nookins --version 2>/dev/null | awk '{print $2}')"
[[ "$NEW_VERSION" == "0.47.0-alpha.2" ]] || rollback "post-swap version is $NEW_VERSION, expected 0.47.0-alpha.2"
checkpoint complete

printf '\nupdate-to-0.47: done. %s upgraded %s -> %s.\n' "$HOME_DIR" "$FROM_VERSION" "$NEW_VERSION"
printf 'Your previous installation is preserved at %s; remove it once you have confirmed the upgrade.\n' "$BACKUP"
printf 'Recovery/download files are in %s.\n' "$WORK"
"""
# END EMBEDDED BRIDGE


def present(path):
    return os.path.lexists(path)


def checked_directory(path):
    """Reject symlinked, foreign-owned or writable ancestor directories."""
    if path == Path('/') or not path.is_absolute() or '..' in path.parts:
        raise ValueError('Choose an absolute home path without .. components')
    current = Path('/')
    for part in path.parts[1:]:
        current /= part
        info = current.lstat()
        if (not stat.S_ISDIR(info.st_mode) or info.st_uid not in (0, os.geteuid())
                or (info.st_mode & 0o022 and not info.st_mode & stat.S_ISVTX)):
            raise ValueError(f'Unsafe directory; preserve it for inspection: {current}')
    if path.stat().st_uid != os.geteuid():
        raise ValueError(f'Directory must belong to the current user: {path}')


def keep_or_create(path, data):
    """Never replace existing bytes, symlinks, hard links or unsafe files."""
    checked_directory(path.parent)
    try:
        descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
    except FileExistsError:
        descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
        with os.fdopen(descriptor, 'rb') as source:
            info = os.fstat(source.fileno())
            if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1
                    or info.st_uid != os.geteuid() or info.st_mode & 0o7022
                    or info.st_size > LIMIT or source.read(LIMIT + 1) != data):
                raise ValueError(f'Existing file differs or is unsafe; preserved: {path}')
    else:
        with os.fdopen(descriptor, 'wb') as output:
            output.write(data)
            output.flush()
            os.fsync(output.fileno())
        directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
        try:
            os.fsync(directory)
        finally:
            os.close(directory)


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        raise ValueError('Refusing a redirected trust-root download')


def download_root():
    request = Request(ORIGIN + '/updates/root.json', headers={'User-Agent': 'nookins-update-repair'})
    with build_opener(NoRedirect).open(request, timeout=30) as response:
        if response.status != 200:
            raise ValueError('Trust-root download did not succeed')
        data = response.read(LIMIT + 1)
    if len(data) > LIMIT or hashlib.sha256(data).hexdigest() != ROOT_SHA256:
        raise ValueError('Public root differs from the reviewed pin; no trust was initialized')
    return data


def read_record(path, limit=16384):
    checked_directory(path.parent)
    descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
    with os.fdopen(descriptor, 'rb') as source:
        info = os.fstat(source.fileno())
        if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid()
                or info.st_nlink != 1 or info.st_mode & 0o7022 or info.st_size > limit):
            raise ValueError(f'Unsafe recovery record: {path}')
        data = source.read(limit + 1)
        if len(data) > limit:
            raise ValueError(f'Oversized recovery record: {path}')
        return data


@contextmanager
def exclusive_repair(home):
    checked_directory(home.parent)
    path = home.parent / ('.' + home.name.removeprefix('.') + '.repair-updates.lock')
    descriptor = os.open(path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
    try:
        info = os.fstat(descriptor)
        if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.geteuid()
                or info.st_nlink != 1 or info.st_mode & 0o7077):
            raise ValueError('Unsafe repair lock; preserve it for inspection')
        try:
            fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except BlockingIOError:
            raise ValueError('Another repair or bridge child is still running; wait for it to finish') from None
        yield descriptor
    finally:
        # Do not explicitly unlock: bridge descendants inherit this descriptor
        # and must keep excluding a new invocation if the parent is interrupted.
        os.close(descriptor)


def installed_version(home):
    checked_directory(home)
    result = subprocess.run([str(home / 'bin/nookins'), '--version'], check=True,
                            capture_output=True, text=True, timeout=15).stdout.strip()
    match = re.fullmatch(r'nookins ((\d+)\.(\d+)\.\d+(?:-[A-Za-z0-9.]+)?)', result)
    if not match:
        raise ValueError('Cannot identify the installed Nookins version')
    return match[1], (int(match[2]), int(match[3]))


def bridge_record(home):
    work = home.parent / ('.' + home.name.removeprefix('.') + '.bridge-recovery-0.47')
    if not present(work):
        return False, False
    checked_directory(work)
    if present(work / 'home'):
        if read_record(work / 'home').decode().strip() != str(home):
            raise ValueError('Bridge recovery belongs to another home; preserving it')
    elif any(present(work / name) for name in ('source-version', 'prepared', 'complete')):
        raise ValueError('Bridge recovery is missing its home identity')
    if not present(work / 'source-version'):
        return False, False
    source = read_record(work / 'source-version').decode().strip()
    if source not in BRIDGE_SOURCES:
        raise ValueError('Bridge recovery has an unsupported source version')
    complete = present(work / 'complete')
    return source if not complete else None, complete


def run_bridge(home, descriptor):
    data = BRIDGE_SCRIPT.encode()
    if hashlib.sha256(data).hexdigest() != BRIDGE_SHA256:
        raise ValueError('Embedded bridge identity mismatch; no upgrade started')
    print('Running the verified 0.47 bridge; the service may restart. '
          'The previous home is preserved at ' + str(home) + '.pre-0.47', flush=True)
    with tempfile.TemporaryDirectory(prefix='.nookins-repair-bridge-', dir=home.parent) as directory:
        script = Path(directory) / 'bridge.sh'
        keep_or_create(script, data)
        subprocess.run(['bash', str(script), '--home', str(home)], check=True,
                       pass_fds=(descriptor,))
    version, _ = installed_version(home)
    if version != BRIDGE_TARGET:
        raise ValueError('Bridge did not activate its exact target; preserve recovery state')


def repair(home, trust_store=None):
    with exclusive_repair(home) as descriptor:
        pending, completed = bridge_record(home)
        if pending:
            if present(home) and installed_version(home)[0] not in (pending, BRIDGE_TARGET):
                raise ValueError('Pending bridge does not match the installed version; refusing rollback or downgrade')
            run_bridge(home, descriptor)
            completed = True
        else:
            version, track = installed_version(home)
            if track < (0, 47):
                if version not in BRIDGE_SOURCES:
                    raise ValueError('Unsupported pre-0.47 source version: ' + version)
                run_bridge(home, descriptor)
                completed = True
        repair_current(home, trust_store, completed)


def repair_optional_config_directories(home):
    """Restore empty optional inputs required by the 0.47 updater.

    Runtime configuration accepts absent resource directories. The released
    updater instead opens each directory unconditionally. Creating an empty
    directory changes no authored configuration or pending-operation records.
    """
    config = home / 'config'
    checked_directory(config)
    descriptor = os.open(config, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
    try:
        identity = os.fstat(descriptor)
        if identity.st_uid != os.geteuid() or identity.st_mode & 0o7022:
            raise ValueError('Unsafe configuration directory; preserved: ' + str(config))
        for name in ('automations.d', 'mcp.d', 'hooks.d'):
            created = False
            try:
                os.mkdir(name, mode=0o700, dir_fd=descriptor)
                created = True
                os.fsync(descriptor)
            except FileExistsError:
                pass
            info = os.stat(name, dir_fd=descriptor, follow_symlinks=False)
            if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.geteuid()
                    or info.st_mode & 0o7022):
                raise ValueError(f'Unsafe optional configuration directory; preserved: {config / name}')
            if created:
                print('Restored empty optional configuration directory: ' + str(config / name), flush=True)
        current = config.lstat()
        if (current.st_dev, current.st_ino) != (identity.st_dev, identity.st_ino):
            raise ValueError('Configuration directory changed during repair; preserve recovery state')
    finally:
        os.close(descriptor)


def repair_current(home, trust_store, bridged):
    repair_optional_config_directories(home)
    binary = home / 'bin/nookins'
    command = [str(binary), '--home', str(home), 'update']
    settings = home / 'maintenance/update.json'
    legacy = Path(str(home) + '.update.json')
    if present(settings) or (present(legacy) and not bridged):
        if trust_store is not None:
            raise ValueError('Settings already exist; refusing to override their saved trust')
        print('Keeping existing settings and trust; refreshing the signed feed.', flush=True)
    else:
        candidates = [home / 'maintenance/trust', Path(str(home) + '.update-trust')]
        if bridged:
            # The exact bridge moved the source home here. Retain its existing
            # trust history in place; never silently initialize a new trust root.
            candidates.append(Path(str(home) + '.pre-0.47') / 'maintenance/trust')
        existing = [path for path in candidates if present(path)]
        if bridged and trust_store is None:
            backup = Path(str(home) + '.pre-0.47')
            saved_paths = [backup / 'maintenance/update.json', legacy]
            saved_path = next((path for path in saved_paths if present(path)), None)
            if saved_path is not None:
                saved = json.loads(read_record(saved_path))
                if (not isinstance(saved, dict) or saved.get('schema') != 1
                        or saved.get('home') != str(home) or not isinstance(saved.get('trust_store'), str)):
                    raise ValueError('Retained settings have a different home/schema; select retained trust explicitly')
                retained_trust = Path(saved['trust_store'])
                managed = home / 'maintenance'
                if retained_trust.is_relative_to(managed):
                    retained_trust = backup / 'maintenance' / retained_trust.relative_to(managed)
                if not present(retained_trust):
                    raise ValueError('Saved trust store is missing; preserve records and restore ' + str(retained_trust))
                trust_store = retained_trust
        if trust_store is None:
            if len(existing) > 1:
                raise ValueError('Multiple trust stores exist; select one with --trust-store: ' + ', '.join(map(str, existing)))
            trust_store = existing[0] if existing else None
        if trust_store is not None:
            checked_directory(trust_store)
            trust_args = ['--trust-store', str(trust_store)]
            print('Keeping existing trust at ' + str(trust_store) + '; retain that directory.', flush=True)
            root = None
        else:
            # A bridge backup can retain prior anti-rollback history. Never silently
            # replace that history with a new enrollment. An explicit existing store
            # is supported without copying or rewriting its durable records.
            backups = [Path(str(home) + '.pre-0.47') / 'maintenance/trust']
            recovery = [home / 'maintenance/state', home / 'maintenance/operations',
                        Path(str(home) + '.update-state'), Path(str(home) + '.update-operations')]
            retained = [path for path in backups + recovery if present(path)]
            if retained:
                hint = ('Rerun with --trust-store ' + shlex.quote(str(backups[0]))
                        + '; that directory remains in use. ' if present(backups[0]) else
                        'Restore the matching trust store and select it with --trust-store. ')
                raise ValueError('Retained records: ' + ', '.join(map(str, retained))
                                 + '. ' + hint + 'Automatic re-enrollment refused')
            root = download_root()  # Verify before creating any local files.
            trust_args = []
        source = Path(str(home) + '.update-source')
        checked_directory(source.parent)
        try:
            source.mkdir(mode=0o700)
        except FileExistsError:
            pass
        checked_directory(source)
        keep_or_create(source / 'repository.json', REPOSITORY)
        if root is not None:
            keep_or_create(source / 'root.json', root)
            trust_args = ['--trusted-root', str(source / 'root.json')]
        subprocess.run(command + ['--configure', '--settings', str(settings),
                       '--repository', str(source / 'repository.json'), '--channel', 'alpha']
                       + trust_args, check=True, timeout=60)
        print('Missing update settings restored. Existing data and recovery history preserved.', flush=True)
    # The native verifier authenticates current metadata, enforces expiry and
    # anti-rollback, and selects the latest compatible published alpha. No version
    # is scraped from website text or accepted from unsigned JSON.
    subprocess.run(command + ['--check'], check=True, timeout=180)
    print('\nFeed check finished; installation compatibility is checked when applying the update.'
          '\nTo install an available update or resume the pending update, run:\n  ' + shlex.join(command))


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--home', type=Path, required=True, help='Existing Nookins home; never a Ward home')
    parser.add_argument('--trust-store', type=Path, help='Explicit existing trust directory when recovery requires it; retained in place')
    args = parser.parse_args()
    os.umask(0o077)
    repair(args.home, args.trust_store)


if __name__ == '__main__':
    try:
        main()
    except (ValueError, KeyError, OSError, subprocess.SubprocessError) as error:
        print(f'Update repair stopped: {error}\nPreserve the home, backup and recovery records. Fix the cause and rerun the same command.', file=sys.stderr)
        sys.exit(1)
    except KeyboardInterrupt:
        print('Repair interrupted. Preserve recovery records and rerun the same command.', file=sys.stderr)
        sys.exit(130)
