#!/usr/bin/env bash
set -Eeuo pipefail
umask 077

STEP="STEP_050M07R13C_EXPORT_VM130_ORCHESTRATION_SOURCE"
PASS_RESULT="PASS_${STEP}"
STOP_RESULT="STOP_${STEP}"
RESULT="$STOP_RESULT"
PHASE="bootstrap"
FAILURE_REASON=""

TOKEN="e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4"
TUNNEL_BASE="${TUNNEL_BASE:-https://receiver-rejected-average-combat.trycloudflare.com}"
ROUTER_OPS_ROOT="${ROUTER_OPS_ROOT:-/opt/router-ops}"
PUBLIC_ROOT="${PUBLIC_ROOT:-$ROUTER_OPS_ROOT/public/r/$TOKEN}"
TS="$(date -u +%Y%m%d-%H%M%S)"
SLUG="vm130_router_ops_source_snapshot"
WORK_DIR="$ROUTER_OPS_ROOT/work/${TS}_${SLUG}"
REPORT_DIR="$PUBLIC_ROOT/${TS}_${SLUG}"
REPORT_URL="$TUNNEL_BASE/r/$TOKEN/${TS}_${SLUG}/"
TOPDIR="wg-paid-vm130-router-ops-${TS}"
STAGE_PARENT="$WORK_DIR/stage"
STAGE_ROOT="$STAGE_PARENT/$TOPDIR"
EXTRACT_VERIFY="$WORK_DIR/extract-verify"
ARCHIVE_NAME="${TOPDIR}.tar.gz"
B64_NAME="${ARCHIVE_NAME}.b64.txt"
ARCHIVE="$REPORT_DIR/$ARCHIVE_NAME"
B64_FILE="$REPORT_DIR/$B64_NAME"
ARCHIVE_URL="${REPORT_URL}${ARCHIVE_NAME}"
B64_URL="${REPORT_URL}${B64_NAME}"
PROGRESS_LOG="$REPORT_DIR/progress.log"

LOCAL_M07_PLAN_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260713-193942_local_architecture_plan_vm101_autonomous_hmn_recovery/"
GLOBAL_PROJECT_PLAN_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260711-123348_global_project_plan_wg_paid/"
ACCESS_MAP_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260711-120734_xs_map_audit_repair_publish/"
XS_MAP_URL="$ACCESS_MAP_URL"
VM100_PUBLIC_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260713-182653_vm100_git_source_097755408853/"
VM101_PUBLIC_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260713-192042_vm101_git_source_027589515a26/"
VM121_PUBLIC_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260713-182700_vm121_git_source_93e0e5a31f09/"
VM101_MODEL_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260713-192030_vm101_model_hmn_download_selector_r13a/vm101-model.txt"
VM101_METHODS_URL="https://receiver-rejected-average-combat.trycloudflare.com/r/$TOKEN/20260713-193942_vm101_methods_machine_git_count_contract_r13b/"

SOURCE_FILE_COUNT=0
SOURCE_SYMLINK_COUNT=0
SOURCE_TOTAL_BYTES=0
SOURCE_MANIFEST_SHA256=""
ARCHIVE_SHA256=""
ARCHIVE_BYTES=0
ARCHIVE_ENTRY_COUNT=0
B64_ROUNDTRIP_SHA256=""
REMOTE_ARCHIVE_SHA256=""
EXTERNAL_HTTP_VERIFY="false"
VM100_CHANGED="false"
VM101_CHANGED="false"
VM121_CHANGED="false"
RUNTIME_IMPACT="false"
GIT_USED="false"
REMOTE_ACCESS_USED="false"
SERVICES_RESTARTED="false"

mkdir -p "$WORK_DIR" "$REPORT_DIR" "$STAGE_ROOT" "$EXTRACT_VERIFY"
cp "$0" "$REPORT_DIR/step-code.sh"
chmod 600 "$REPORT_DIR/step-code.sh"
exec > >(tee -a "$PROGRESS_LOG") 2>&1

log() { printf '>>> [%s] %s\n' "$PHASE" "$*"; }

make_index() {
  {
    printf '<!doctype html><html><head><meta charset="utf-8"><title>%s</title></head><body>\n' "$STEP"
    printf '<h1>%s</h1><ul>\n' "$STEP"
    for f in report.txt facts.json step-code.sh progress.log source-scope.txt included-paths.txt excluded-paths.txt source-manifest.json source-manifest.sha256 file-list.tsv manifest.sha256 verify.txt rollback.sh "$ARCHIVE_NAME" "$B64_NAME"; do
      [ -e "$REPORT_DIR/$f" ] && printf '<li><a href="%s">%s</a></li>\n' "$f" "$f"
    done
    printf '</ul></body></html>\n'
  } > "$REPORT_DIR/index.html"
  chmod 644 "$REPORT_DIR/index.html"
}

write_facts() {
  export STEP RESULT PHASE FAILURE_REASON TS REPORT_URL ARCHIVE_URL B64_URL
  export SOURCE_FILE_COUNT SOURCE_SYMLINK_COUNT SOURCE_TOTAL_BYTES SOURCE_MANIFEST_SHA256
  export ARCHIVE_SHA256 ARCHIVE_BYTES ARCHIVE_ENTRY_COUNT B64_ROUNDTRIP_SHA256 REMOTE_ARCHIVE_SHA256
  export EXTERNAL_HTTP_VERIFY VM100_CHANGED VM101_CHANGED VM121_CHANGED RUNTIME_IMPACT
  export GIT_USED REMOTE_ACCESS_USED SERVICES_RESTARTED ROUTER_OPS_ROOT TOPDIR
  export LOCAL_M07_PLAN_URL GLOBAL_PROJECT_PLAN_URL ACCESS_MAP_URL XS_MAP_URL
  export VM100_PUBLIC_URL VM101_PUBLIC_URL VM121_PUBLIC_URL VM101_MODEL_URL VM101_METHODS_URL
  python3 - "$REPORT_DIR/facts.json" <<'PY'
import json, os, sys

def b(name):
    return os.environ.get(name, '').lower() == 'true'

def i(name):
    value = os.environ.get(name, '')
    return int(value) if value.isdigit() else None

data = {
    "schema": "router-step-facts-v1",
    "step": os.environ.get("STEP"),
    "timestamp_utc": os.environ.get("TS"),
    "assessment": {
        "decision": os.environ.get("RESULT"),
        "phase": os.environ.get("PHASE"),
        "failure_reason": os.environ.get("FAILURE_REASON") or None,
    },
    "operation": {
        "type": "read_only_vm130_orchestration_source_export",
        "source_root": os.environ.get("ROUTER_OPS_ROOT"),
        "archive_top_directory": os.environ.get("TOPDIR"),
        "git_used": b("GIT_USED"),
        "remote_access_used": b("REMOTE_ACCESS_USED"),
        "services_restarted": b("SERVICES_RESTARTED"),
        "runtime_impact": b("RUNTIME_IMPACT"),
    },
    "source_snapshot": {
        "regular_file_count": i("SOURCE_FILE_COUNT"),
        "symlink_count": i("SOURCE_SYMLINK_COUNT"),
        "total_regular_file_bytes": i("SOURCE_TOTAL_BYTES"),
        "source_manifest_sha256": os.environ.get("SOURCE_MANIFEST_SHA256") or None,
        "archive_entry_count": i("ARCHIVE_ENTRY_COUNT"),
        "archive_bytes": i("ARCHIVE_BYTES"),
        "archive_sha256": os.environ.get("ARCHIVE_SHA256") or None,
        "base64_roundtrip_sha256": os.environ.get("B64_ROUNDTRIP_SHA256") or None,
        "remote_archive_sha256": os.environ.get("REMOTE_ARCHIVE_SHA256") or None,
        "external_http_verify": b("EXTERNAL_HTTP_VERIFY"),
    },
    "safety": {
        "vm100_changed": b("VM100_CHANGED"),
        "vm101_changed": b("VM101_CHANGED"),
        "vm121_changed": b("VM121_CHANGED"),
        "runtime_impact": b("RUNTIME_IMPACT"),
        "remote_access_used": b("REMOTE_ACCESS_USED"),
    },
    "links": {
        "step_report": os.environ.get("REPORT_URL"),
        "archive": os.environ.get("ARCHIVE_URL"),
        "archive_base64": os.environ.get("B64_URL"),
        "local_m07_plan": os.environ.get("LOCAL_M07_PLAN_URL"),
        "global_project_plan": os.environ.get("GLOBAL_PROJECT_PLAN_URL"),
        "access_map": os.environ.get("ACCESS_MAP_URL"),
        "xs_map": os.environ.get("XS_MAP_URL"),
        "vm100": os.environ.get("VM100_PUBLIC_URL"),
        "vm101": os.environ.get("VM101_PUBLIC_URL"),
        "vm121": os.environ.get("VM121_PUBLIC_URL"),
        "vm101_model": os.environ.get("VM101_MODEL_URL"),
        "vm101_methods": os.environ.get("VM101_METHODS_URL"),
    },
}
with open(sys.argv[1], "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)
    f.write("\n")
PY
  chmod 644 "$REPORT_DIR/facts.json"
}

write_report() {
  cat > "$REPORT_DIR/report.txt" <<EOF_REPORT
RESULT=$RESULT
STEP=$STEP
TIMESTAMP_UTC=$TS
PHASE=$PHASE
FAILURE_REASON=${FAILURE_REASON:-NONE}

PURPOSE:
- Export the active VM130 router-ops orchestration source into the same transport form used for Machine Git sources: tar.gz plus base64 text.
- Publish the source snapshot through the existing Quick Tunnel.
- Do not modify VM100, VM101, VM121, Git repositories, services, routes or runtime state.

SCOPE:
- /opt/router-ops source and configuration files, excluding runtime/public/work/cache/archive/log/secret areas.
- router-* executables and libraries under /usr/local/bin, /usr/local/sbin and /usr/local/lib.
- Existing absolute orchestration dependencies referenced by collected text files, restricted to those same source roots.
- Sensitive key/container formats and private-key material are excluded or cause STOP.

RESULTS:
- regular_file_count=$SOURCE_FILE_COUNT
- symlink_count=$SOURCE_SYMLINK_COUNT
- total_regular_file_bytes=$SOURCE_TOTAL_BYTES
- source_manifest_sha256=${SOURCE_MANIFEST_SHA256:-UNKNOWN}
- archive_entry_count=$ARCHIVE_ENTRY_COUNT
- archive_bytes=$ARCHIVE_BYTES
- archive_sha256=${ARCHIVE_SHA256:-UNKNOWN}
- base64_roundtrip_sha256=${B64_ROUNDTRIP_SHA256:-UNKNOWN}
- remote_archive_sha256=${REMOTE_ARCHIVE_SHA256:-UNKNOWN}
- external_http_verify=$EXTERNAL_HTTP_VERIFY

SAFETY:
- GIT_USED=$GIT_USED
- REMOTE_ACCESS_USED=$REMOTE_ACCESS_USED
- SERVICES_RESTARTED=$SERVICES_RESTARTED
- RUNTIME_IMPACT=$RUNTIME_IMPACT
- VM100_CHANGED=$VM100_CHANGED
- VM101_CHANGED=$VM101_CHANGED
- VM121_CHANGED=$VM121_CHANGED

ROLLBACK:
- No runtime rollback is required.
- rollback.sh removes only this timestamped public snapshot and its temporary work directory.

STEP_REPORT_URL=$REPORT_URL
VM130_SOURCE_URL=$REPORT_URL
VM130_SOURCE_ARCHIVE_URL=$ARCHIVE_URL
VM130_SOURCE_ARCHIVE_B64_URL=$B64_URL
LOCAL_M07_PLAN_URL=$LOCAL_M07_PLAN_URL
GLOBAL_PROJECT_PLAN_URL=$GLOBAL_PROJECT_PLAN_URL
ACCESS_MAP_URL=$ACCESS_MAP_URL
XS_MAP_URL=$XS_MAP_URL
VM100_PUBLIC_URL=$VM100_PUBLIC_URL
VM101_PUBLIC_URL=$VM101_PUBLIC_URL
VM121_PUBLIC_URL=$VM121_PUBLIC_URL
VM101_MODEL_URL=$VM101_MODEL_URL
VM101_METHODS_URL=$VM101_METHODS_URL
FACTS_URL=${REPORT_URL}facts.json
VERIFY_URL=${REPORT_URL}verify.txt
ROLLBACK_URL=${REPORT_URL}rollback.sh
ARCHITECTURE_PLAN_URL=$LOCAL_M07_PLAN_URL
EOF_REPORT
  chmod 644 "$REPORT_DIR/report.txt"
}

write_materials() {
  write_facts
  write_report
  make_index
}

print_final() {
  printf '\nRESULT=%s\n' "$RESULT"
  printf 'STEP_REPORT_URL=%s\n' "$REPORT_URL"
  printf 'VM130_SOURCE_URL=%s\n' "$REPORT_URL"
  printf 'VM130_SOURCE_ARCHIVE_URL=%s\n' "$ARCHIVE_URL"
  printf 'VM130_SOURCE_ARCHIVE_B64_URL=%s\n' "$B64_URL"
  printf 'LOCAL_M07_PLAN_URL=%s\n' "$LOCAL_M07_PLAN_URL"
  printf 'GLOBAL_PROJECT_PLAN_URL=%s\n' "$GLOBAL_PROJECT_PLAN_URL"
  printf 'ACCESS_MAP_URL=%s\n' "$ACCESS_MAP_URL"
  printf 'XS_MAP_URL=%s\n' "$XS_MAP_URL"
  printf 'VM100_PUBLIC_URL=%s\n' "$VM100_PUBLIC_URL"
  printf 'VM101_PUBLIC_URL=%s\n' "$VM101_PUBLIC_URL"
  printf 'VM121_PUBLIC_URL=%s\n' "$VM121_PUBLIC_URL"
  printf 'VM101_MODEL_URL=%s\n' "$VM101_MODEL_URL"
  printf 'VM101_METHODS_URL=%s\n' "$VM101_METHODS_URL"
  printf 'FACTS_URL=%sfacts.json\n' "$REPORT_URL"
  printf 'VERIFY_URL=%sverify.txt\n' "$REPORT_URL"
  printf 'ROLLBACK_URL=%srollback.sh\n' "$REPORT_URL"
  printf 'ARCHITECTURE_PLAN_URL=%s\n' "$LOCAL_M07_PLAN_URL"
}

on_error() {
  local rc=$?
  trap - ERR
  set +e
  RESULT="$STOP_RESULT"
  FAILURE_REASON="${FAILURE_REASON:-COMMAND_FAILED_IN_${PHASE}_RC_${rc}}"
  printf '>>> [STOP] phase=%s rc=%s reason=%s\n' "$PHASE" "$rc" "$FAILURE_REASON"
  write_materials
  print_final
  exit "$rc"
}
trap on_error ERR

PHASE="preflight"
log "Checking local export prerequisites"
for cmd in bash python3 find tar gzip base64 sha256sum curl stat readlink cp sort awk; do
  command -v "$cmd" >/dev/null || { FAILURE_REASON="MISSING_COMMAND_${cmd}"; false; }
done
[ -d "$ROUTER_OPS_ROOT" ] || { FAILURE_REASON="ROUTER_OPS_ROOT_MISSING"; false; }
[ -d "$PUBLIC_ROOT" ] && [ -w "$PUBLIC_ROOT" ] || { FAILURE_REASON="PUBLIC_ROOT_NOT_WRITABLE"; false; }

cat > "$REPORT_DIR/source-scope.txt" <<EOF_SCOPE
VM130 orchestration source export scope

Included seeds:
- All regular files and symlinks below $ROUTER_OPS_ROOT except excluded runtime/state directories and sensitive path patterns.
- /usr/local/bin/router-*
- /usr/local/sbin/router-*
- /usr/local/lib/router-*
- Resolved PATH commands: router-machine-close, router-run, router-step, access-map, when present.
- Existing absolute dependencies referenced by collected text files, restricted to:
  $ROUTER_OPS_ROOT
  /usr/local/bin
  /usr/local/sbin
  /usr/local/lib

Excluded directory names:
- public, work, cache, caches, archive, archives, backup, backups, log, logs, tmp, temp
- run, runs, state, states, report, reports, artifact, artifacts, snapshot, snapshots
- download, downloads, clone, clones, repo, repos, repository, repositories, mirror, mirrors
- .git, .hg, .svn, .ssh, __pycache__, node_modules, venv, .venv
- secrets, secret, credentials

Excluded file patterns:
- .env and .env.*
- id_*, authorized_keys, known_hosts
- *.key, *.pem, *.p12, *.pfx, *.kdbx
- *.tar, *.tar.gz, *.tgz, *.zip, *.7z, *.xz, *.zst, *.iso, *.img, *.qcow2
- *.log, *.sqlite, *.sqlite3, *.db, *.dump

Safety:
- No SSH command is executed.
- No Git command is executed.
- No service is restarted.
- No source file is modified.
- A file beginning with a private-key PEM/OpenSSH marker causes STOP.
EOF_SCOPE
chmod 644 "$REPORT_DIR/source-scope.txt"

PHASE="collect"
log "Collecting local VM130 orchestration source"
export ROUTER_OPS_ROOT STAGE_ROOT REPORT_DIR
python3 <<'PY'
import fnmatch
import hashlib
import json
import os
import re
import shutil
import stat
from pathlib import Path

root = Path(os.environ["ROUTER_OPS_ROOT"]).resolve()
stage_root = Path(os.environ["STAGE_ROOT"])
report_dir = Path(os.environ["REPORT_DIR"])

excluded_dir_names = {
    "public", "work", "cache", "caches", "archive", "archives", "backup", "backups",
    "log", "logs", "tmp", "temp", "run", "runs", "state", "states", "report", "reports",
    "artifact", "artifacts", "snapshot", "snapshots", "download", "downloads", "clone", "clones",
    "repo", "repos", "repository", "repositories", "mirror", "mirrors",
    ".git", ".hg", ".svn", ".ssh", "__pycache__", "node_modules", "venv", ".venv",
    "secrets", "secret", "credentials",
}
excluded_file_patterns = [
    ".env", ".env.*", "id_*", "authorized_keys", "known_hosts",
    "*.key", "*.pem", "*.p12", "*.pfx", "*.kdbx",
    "*.tar", "*.tar.gz", "*.tgz", "*.zip", "*.7z", "*.xz", "*.zst",
    "*.iso", "*.img", "*.qcow2", "*.log", "*.sqlite", "*.sqlite3", "*.db", "*.dump",
]
allowed_dependency_roots = [
    root,
    Path("/usr/local/bin"),
    Path("/usr/local/sbin"),
    Path("/usr/local/lib"),
]
absolute_ref = re.compile(rb"/(?:opt/router-ops|usr/local/(?:bin|sbin|lib))(?:/[A-Za-z0-9._@+,:=-]+)+")
private_key_starts = (
    b"-----BEGIN OPENSSH PRIVATE KEY-----",
    b"-----BEGIN RSA PRIVATE KEY-----",
    b"-----BEGIN DSA PRIVATE KEY-----",
    b"-----BEGIN EC PRIVATE KEY-----",
    b"-----BEGIN PRIVATE KEY-----",
)

included = set()
excluded = []

def under_allowed(path: Path) -> bool:
    try:
        resolved = path.resolve(strict=False)
    except OSError:
        resolved = path
    for base in allowed_dependency_roots:
        try:
            resolved.relative_to(base.resolve(strict=False))
            return True
        except ValueError:
            continue
    return False

def sensitive_path(path: Path) -> bool:
    if any(part in excluded_dir_names for part in path.parts):
        return True
    name = path.name
    return any(fnmatch.fnmatch(name, pat) for pat in excluded_file_patterns)

def add(path: Path, reason: str) -> bool:
    try:
        if not path.is_file() and not path.is_symlink():
            return False
    except OSError:
        return False
    if not under_allowed(path):
        excluded.append({"path": str(path), "reason": f"outside_allowed_roots:{reason}"})
        return False
    if sensitive_path(path):
        excluded.append({"path": str(path), "reason": f"sensitive_or_runtime_pattern:{reason}"})
        return False
    included.add(str(path))
    return True

for current, dirs, files in os.walk(root, topdown=True, followlinks=False):
    dirs[:] = sorted(d for d in dirs if d not in excluded_dir_names)
    current_path = Path(current)
    for name in sorted(files):
        add(current_path / name, "router_ops_tree")
    for name in sorted(dirs):
        p = current_path / name
        if p.is_symlink():
            add(p, "router_ops_symlink")

for base in (Path("/usr/local/bin"), Path("/usr/local/sbin"), Path("/usr/local/lib")):
    if not base.exists():
        continue
    for path in sorted(base.glob("router-*")):
        add(path, "router_prefix")

for command in ("router-machine-close", "router-run", "router-step", "access-map"):
    resolved = shutil.which(command)
    if resolved:
        add(Path(resolved), f"resolved_command:{command}")

# Add referenced local orchestration dependencies for a few rounds.
for _ in range(6):
    added = 0
    for item in sorted(included):
        path = Path(item)
        if path.is_symlink():
            target = Path(os.readlink(path))
            if not target.is_absolute():
                target = path.parent / target
            if add(target, f"symlink_target:{path}"):
                added += 1
            continue
        try:
            if path.stat().st_size > 8 * 1024 * 1024:
                continue
            data = path.read_bytes()
        except (OSError, PermissionError):
            continue
        if b"\x00" in data[:4096]:
            continue
        for match in absolute_ref.findall(data):
            dep = Path(match.decode("utf-8", "ignore").rstrip(".,;:)]}'\""))
            if add(dep, f"absolute_reference:{path}"):
                added += 1
    if added == 0:
        break

records = []
private_key_hits = []
for item in sorted(included):
    src = Path(item)
    rel = Path(str(src).lstrip("/"))
    dst = stage_root / rel
    dst.parent.mkdir(parents=True, exist_ok=True)
    if src.is_symlink():
        target = os.readlink(src)
        if dst.exists() or dst.is_symlink():
            dst.unlink()
        os.symlink(target, dst)
        records.append({
            "path": rel.as_posix(),
            "type": "symlink",
            "target": target,
            "mode": oct(src.lstat().st_mode & 0o7777),
            "size": 0,
            "sha256": hashlib.sha256(target.encode()).hexdigest(),
        })
        continue
    with src.open("rb") as f:
        prefix = f.read(4096).lstrip()
    if any(prefix.startswith(marker) for marker in private_key_starts):
        private_key_hits.append(str(src))
        continue
    shutil.copy2(src, dst, follow_symlinks=False)
    h = hashlib.sha256()
    size = 0
    with src.open("rb") as f:
        while True:
            chunk = f.read(1024 * 1024)
            if not chunk:
                break
            h.update(chunk)
            size += len(chunk)
    records.append({
        "path": rel.as_posix(),
        "type": "file",
        "mode": oct(src.stat().st_mode & 0o7777),
        "size": size,
        "sha256": h.hexdigest(),
    })

if private_key_hits:
    (report_dir / "private-key-hits.txt").write_text("\n".join(private_key_hits) + "\n", encoding="utf-8")
    raise SystemExit("private key material detected; export stopped")
if not records:
    raise SystemExit("no orchestration source files collected")

manifest = {
    "schema": "vm130-orchestration-source-manifest-v1",
    "source_root": str(root),
    "records": records,
}
manifest_path = report_dir / "source-manifest.json"
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

(report_dir / "included-paths.txt").write_text(
    "".join(record["path"] + "\n" for record in records), encoding="utf-8"
)
(report_dir / "excluded-paths.txt").write_text(
    "".join(f"{entry['path']}\t{entry['reason']}\n" for entry in sorted(excluded, key=lambda x: (x['path'], x['reason']))),
    encoding="utf-8",
)
with (report_dir / "file-list.tsv").open("w", encoding="utf-8") as f:
    f.write("type\tmode\tsize\tsha256\tpath_or_target\n")
    for record in records:
        target = record.get("target", "")
        tail = record["path"] if not target else f"{record['path']} -> {target}"
        f.write(f"{record['type']}\t{record['mode']}\t{record['size']}\t{record['sha256']}\t{tail}\n")
PY

SOURCE_FILE_COUNT="$(python3 - "$REPORT_DIR/source-manifest.json" <<'PY'
import json, sys
m=json.load(open(sys.argv[1], encoding='utf-8'))
print(sum(1 for r in m['records'] if r['type']=='file'))
PY
)"
SOURCE_SYMLINK_COUNT="$(python3 - "$REPORT_DIR/source-manifest.json" <<'PY'
import json, sys
m=json.load(open(sys.argv[1], encoding='utf-8'))
print(sum(1 for r in m['records'] if r['type']=='symlink'))
PY
)"
SOURCE_TOTAL_BYTES="$(python3 - "$REPORT_DIR/source-manifest.json" <<'PY'
import json, sys
m=json.load(open(sys.argv[1], encoding='utf-8'))
print(sum(r['size'] for r in m['records'] if r['type']=='file'))
PY
)"
SOURCE_MANIFEST_SHA256="$(sha256sum "$REPORT_DIR/source-manifest.json" | awk '{print $1}')"
printf '%s  %s\n' "$SOURCE_MANIFEST_SHA256" source-manifest.json > "$REPORT_DIR/source-manifest.sha256"

PHASE="archive"
log "Creating tar.gz and base64 transport"
tar --sort=name --mtime='UTC 1970-01-01' --owner=0 --group=0 --numeric-owner \
  -C "$STAGE_PARENT" -czf "$ARCHIVE" "$TOPDIR"
ARCHIVE_SHA256="$(sha256sum "$ARCHIVE" | awk '{print $1}')"
ARCHIVE_BYTES="$(stat -c '%s' "$ARCHIVE")"
base64 -w 76 "$ARCHIVE" > "$B64_FILE"
B64_ROUNDTRIP_SHA256="$(base64 -d "$B64_FILE" | sha256sum | awk '{print $1}')"
[ "$B64_ROUNDTRIP_SHA256" = "$ARCHIVE_SHA256" ] || { FAILURE_REASON="BASE64_ROUNDTRIP_SHA_MISMATCH"; false; }

PHASE="verify-archive"
log "Verifying extracted archive against the source manifest"
tar -xzf "$ARCHIVE" -C "$EXTRACT_VERIFY"
export VERIFY_ROOT="$EXTRACT_VERIFY/$TOPDIR"
python3 - "$REPORT_DIR/source-manifest.json" <<'PY'
import hashlib
import json
import os
import sys
from pathlib import Path

manifest=json.load(open(sys.argv[1], encoding='utf-8'))
root=Path(os.environ['VERIFY_ROOT'])
expected=set()
for r in manifest['records']:
    path=root / r['path']
    expected.add(r['path'])
    if r['type']=='symlink':
        if not path.is_symlink():
            raise SystemExit(f"missing symlink: {r['path']}")
        target=os.readlink(path)
        if target != r['target']:
            raise SystemExit(f"symlink target mismatch: {r['path']}")
        digest=hashlib.sha256(target.encode()).hexdigest()
    else:
        if not path.is_file() or path.is_symlink():
            raise SystemExit(f"missing regular file: {r['path']}")
        h=hashlib.sha256()
        with path.open('rb') as f:
            for chunk in iter(lambda: f.read(1024*1024), b''):
                h.update(chunk)
        digest=h.hexdigest()
    if digest != r['sha256']:
        raise SystemExit(f"sha256 mismatch: {r['path']}")
actual=set()
for p in root.rglob('*'):
    if p.is_file() or p.is_symlink():
        actual.add(p.relative_to(root).as_posix())
if actual != expected:
    missing=sorted(expected-actual)
    extra=sorted(actual-expected)
    raise SystemExit(f"tree mismatch missing={missing} extra={extra}")
print(f"VERIFY_SOURCE_RECORDS={len(expected)}")
PY
ARCHIVE_ENTRY_COUNT="$(tar -tzf "$ARCHIVE" | awk 'substr($0,length($0),1)!="/"{n++} END{print n+0}')"
EXPECTED_ENTRY_COUNT="$((SOURCE_FILE_COUNT + SOURCE_SYMLINK_COUNT))"
[ "$ARCHIVE_ENTRY_COUNT" -eq "$EXPECTED_ENTRY_COUNT" ] || { FAILURE_REASON="ARCHIVE_ENTRY_COUNT_MISMATCH"; false; }

cat > "$REPORT_DIR/manifest.sha256" <<EOF_MANIFEST
$ARCHIVE_SHA256  $ARCHIVE_NAME
$(sha256sum "$B64_FILE" | awk -v n="$B64_NAME" '{print $1"  "n}')
$SOURCE_MANIFEST_SHA256  source-manifest.json
$(sha256sum "$REPORT_DIR/file-list.tsv" | awk '{print $1"  file-list.tsv"}')
$(sha256sum "$REPORT_DIR/included-paths.txt" | awk '{print $1"  included-paths.txt"}')
EOF_MANIFEST
chmod 644 "$REPORT_DIR"/*.txt "$REPORT_DIR"/*.json "$REPORT_DIR"/*.sha256 "$ARCHIVE" "$B64_FILE"

cat > "$REPORT_DIR/rollback.sh" <<EOF_ROLLBACK
#!/usr/bin/env bash
set -Eeuo pipefail
rm -rf -- '$REPORT_DIR' '$WORK_DIR'
printf 'ROLLBACK=PASS_REMOVE_VM130_SOURCE_SNAPSHOT_%s\n' '$TS'
EOF_ROLLBACK
chmod 700 "$REPORT_DIR/rollback.sh"

PHASE="external-verify"
log "Verifying published archive through the current Quick Tunnel"
REMOTE_ARCHIVE_SHA256="$(curl -fsSL --retry 3 --connect-timeout 10 --max-time 180 "$ARCHIVE_URL" | sha256sum | awk '{print $1}')"
[ "$REMOTE_ARCHIVE_SHA256" = "$ARCHIVE_SHA256" ] || { FAILURE_REASON="REMOTE_ARCHIVE_SHA_MISMATCH"; false; }
curl -fsSL --retry 3 --connect-timeout 10 --max-time 30 "${REPORT_URL}source-manifest.json" >/dev/null
EXTERNAL_HTTP_VERIFY="true"

PHASE="finalize"
RESULT="$PASS_RESULT"
cat > "$REPORT_DIR/verify.txt" <<EOF_VERIFY
RESULT=$RESULT
SOURCE_FILE_COUNT=$SOURCE_FILE_COUNT
SOURCE_SYMLINK_COUNT=$SOURCE_SYMLINK_COUNT
SOURCE_TOTAL_BYTES=$SOURCE_TOTAL_BYTES
SOURCE_MANIFEST_SHA256=$SOURCE_MANIFEST_SHA256
ARCHIVE_ENTRY_COUNT=$ARCHIVE_ENTRY_COUNT
ARCHIVE_BYTES=$ARCHIVE_BYTES
ARCHIVE_SHA256=$ARCHIVE_SHA256
B64_ROUNDTRIP_SHA256=$B64_ROUNDTRIP_SHA256
REMOTE_ARCHIVE_SHA256=$REMOTE_ARCHIVE_SHA256
EXTERNAL_HTTP_VERIFY=$EXTERNAL_HTTP_VERIFY
GIT_USED=$GIT_USED
REMOTE_ACCESS_USED=$REMOTE_ACCESS_USED
SERVICES_RESTARTED=$SERVICES_RESTARTED
RUNTIME_IMPACT=$RUNTIME_IMPACT
VM100_CHANGED=$VM100_CHANGED
VM101_CHANGED=$VM101_CHANGED
VM121_CHANGED=$VM121_CHANGED
EOF_VERIFY
chmod 644 "$REPORT_DIR/verify.txt"
write_materials
print_final
