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

[ "$(id -un)" = "ops" ] || {
  echo "STOP: запускать на router-ops пользователем ops"
  exit 1
}

STEP="STEP_050M07D3_FREEZE_EXACT_ENDPOINTS_BEFORE_SCHEDULED_REFRESH"
PASS_DECISION="PASS_${STEP}"

TOKEN="e94a0859747d7b96f29c7fdafc2d0351ba603bb0a7e9e5a4"
PUBLIC_BASE="https://helena-background-beam-harry.trycloudflare.com/r/${TOKEN}"

ROOT="/opt/router-ops"
PUBROOT="${ROOT}/public/r/${TOKEN}"

D2_DIR="${PUBROOT}/20260711-211307_step050m07d2_freeze_pre0420_evidence"

ARCHITECTURE_PLAN="${PUBLIC_BASE}/20260711-181158_local_architecture_plan_vm101_autonomous_hmn_recovery/"
XS_MAP="${PUBLIC_BASE}/20260711-120734_xs_map_audit_repair_publish/"
GLOBAL_PROJECT_PLAN="${PUBLIC_BASE}/20260711-123348_global_project_plan_wg_paid/"

TS="$(date -u +%Y%m%d-%H%M%S)"
REPORT_SLUG="${TS}_step050m07d3_freeze_exact_endpoints_before_scheduled_refresh"
REPORT_DIR="${PUBROOT}/${REPORT_SLUG}"

TRYCF_REPORT="${PUBLIC_BASE}/${REPORT_SLUG}/"
REPORT_TXT="${TRYCF_REPORT}report.txt"
FACTS_JSON="${TRYCF_REPORT}facts.json"

mkdir -p "$REPORT_DIR" "$REPORT_DIR/sources"

# Точный пользовательский STEP сохраняется до проверок и SSH.
cp -a "$0" "$REPORT_DIR/step.sh"
chmod 600 "$REPORT_DIR/step.sh"

PROGRESS_LOG="$REPORT_DIR/progress.log"
: > "$PROGRESS_LOG"

CURRENT_STAGE="initialization"
LAST_SUCCESS="step_saved"
VM101_RC="NOT_RUN"

stage() {
  CURRENT_STAGE="$1"

  echo
  echo ">>> [$1] $2" | tee -a "$PROGRESS_LOG"
  date -u '+    utc=%Y-%m-%dT%H:%M:%SZ' |
    tee -a "$PROGRESS_LOG"
}

mark_success() {
  LAST_SUCCESS="$1"
  echo "last_success=$LAST_SUCCESS" >> "$PROGRESS_LOG"
}

print_links() {
  echo
  echo "TRYCF_REPORT=$TRYCF_REPORT"
  echo "REPORT_TXT=$REPORT_TXT"
  echo "FACTS_JSON=$FACTS_JSON"
  echo "ARCHITECTURE_PLAN=$ARCHITECTURE_PLAN"
  echo "XS_MAP=$XS_MAP"
  echo "GLOBAL_PROJECT_PLAN=$GLOBAL_PROJECT_PLAN"
}

create_index() {
  cat > "$REPORT_DIR/index.html" <<EOF
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>${STEP}</title>
</head>
<body style="font-family:system-ui;max-width:1100px;margin:40px auto;padding:0 20px">
<h1>${STEP}</h1>
<ul>
<li><a href="report.txt">report.txt</a></li>
<li><a href="facts.json">facts.json</a></li>
<li><a href="exact-baseline.json">exact-baseline.json</a></li>
<li><a href="comparison-contract.json">comparison-contract.json</a></li>
<li><a href="vm101.txt">vm101.txt</a></li>
<li><a href="vm101.stderr">vm101.stderr</a></li>
<li><a href="vm101.sh">vm101.sh</a></li>
<li><a href="step.sh">step.sh</a></li>
<li><a href="sources/d2-facts.json">D2 facts</a></li>
<li><a href="sources/d2-runtime-baseline.json">D2 baseline</a></li>
<li><a href="sources/d2-scheduler-contract.txt">D2 scheduler</a></li>
</ul>
</body>
</html>
EOF
}

write_stop() {
  local reason="$1"
  local rc="$2"
  local line="$3"

  python3 - \
    "$REPORT_DIR" \
    "$STEP" \
    "$reason" \
    "$rc" \
    "$line" \
    "$CURRENT_STAGE" \
    "$LAST_SUCCESS" \
    "$VM101_RC" \
    > "$REPORT_DIR/diagnostic.json" <<'PY'
import json
import sys
from pathlib import Path

(
    report_dir,
    step,
    reason,
    rc,
    line,
    stage,
    last_success,
    vm101_rc,
) = sys.argv[1:]

root = Path(report_dir)

def tail(path: Path, limit=80000):
    if not path.exists() or not path.is_file():
        return None
    return path.read_text(
        encoding="utf-8",
        errors="replace",
    )[-limit:]

streams = {}

for pattern in ("*.txt", "*.stderr", "*.log"):
    for path in sorted(root.glob(pattern)):
        if path.name in {"report.txt", "progress.log"}:
            continue
        streams[path.name] = {
            "size_bytes": path.stat().st_size,
            "tail": tail(path),
        }

combined = "\n".join(
    item["tail"] or ""
    for item in streams.values()
)

if "endpoint_unresolved=" in combined:
    classification = "WIREGUARD_ENDPOINT_STILL_UNRESOLVED"
elif "strict_all=false" in combined:
    classification = "PRE_SCHEDULED_REFRESH_BASELINE_DEGRADED"
elif "schedule_missing=" in combined:
    classification = "SCHEDULE_CONTRACT_NOT_FOUND"
elif vm101_rc not in {"NOT_RUN", "0"}:
    classification = "VM101_READONLY_SNAPSHOT_FAILED"
else:
    classification = "LOCAL_PARSER_OR_PUBLICATION_FAILURE"

print(json.dumps({
    "schema": "router-step-inline-diagnostic-v11",
    "step": step,
    "failure": {
        "reason": reason,
        "rc": int(rc),
        "line": int(line),
        "stage": stage,
        "last_success": last_success,
    },
    "command_results": {
        "vm101_rc": vm101_rc,
    },
    "automatic_classification": classification,
    "safety": {
        "read_only": True,
        "refresh_ran": False,
        "rebalance_ran": False,
        "network_changed": False,
        "services_changed": False,
        "state_changed": False,
        "timer_changed": False,
        "plan_changed": False,
        "direct_failopen_changed": False,
    },
    "captured_streams": streams,
    "recommended_next_step": (
        "После scheduled refresh выполнить только read-only comparison "
        "с exact-baseline.json."
    ),
}, ensure_ascii=False, indent=2))
PY

  cat > "$REPORT_DIR/report.txt" <<EOF
=== ${STEP} RESULT ===
step=${STEP}
decision=STOP_${STEP}_${reason}
all_ok=false
mode=M07_EXACT_PRE_SCHEDULED_REFRESH_FREEZE
error_rc=${rc}
error_line=${line}
failed_stage=${CURRENT_STAGE}
last_success=${LAST_SUCCESS}

command_results:
  vm101_rc=${VM101_RC}

safety:
  read_only=true
  refresh_ran=false
  rebalance_ran=false
  network_changed=false
  services_changed=false
  state_changed=false
  timer_changed=false
  plan_changed=false
  direct_failopen_changed=false

plan:
  current_milestone=M07
  milestone_completed=false
  milestone_changed=false

TRYCF_REPORT=${TRYCF_REPORT}
REPORT_TXT=${REPORT_TXT}
FACTS_JSON=${FACTS_JSON}
ARCHITECTURE_PLAN=${ARCHITECTURE_PLAN}
XS_MAP=${XS_MAP}
GLOBAL_PROJECT_PLAN=${GLOBAL_PROJECT_PLAN}
EOF

  python3 - \
    "$REPORT_DIR/diagnostic.json" \
    "$STEP" \
    "$reason" \
    "$rc" \
    "$line" \
    "$CURRENT_STAGE" \
    "$LAST_SUCCESS" \
    "$TRYCF_REPORT" \
    "$REPORT_TXT" \
    "$FACTS_JSON" \
    "$ARCHITECTURE_PLAN" \
    "$XS_MAP" \
    "$GLOBAL_PROJECT_PLAN" \
    > "$REPORT_DIR/facts.json" <<'PY'
import json
import sys

(
    diagnostic_path,
    step,
    reason,
    rc,
    line,
    stage,
    last_success,
    report,
    report_txt,
    facts_json,
    architecture,
    xs_map,
    global_plan,
) = sys.argv[1:]

with open(diagnostic_path, encoding="utf-8") as source:
    diagnostic = json.load(source)

print(json.dumps({
    "schema": "router-step-facts-v1",
    "step": step,
    "assessment": {
        "decision": f"STOP_{step}_{reason}",
        "all_ok": False,
        "error_rc": int(rc),
        "error_line": int(line),
        "failed_stage": stage,
        "last_success": last_success,
    },
    "inline_diagnostic": diagnostic,
    "safety": diagnostic["safety"],
    "plan": {
        "current_milestone": "M07",
        "milestone_completed": False,
        "milestone_changed": False,
    },
    "publish": {
        "trycf_report": report,
        "report_txt": report_txt,
        "facts_json": facts_json,
        "architecture_plan": architecture,
        "xs_map": xs_map,
        "global_project_plan": global_plan,
    },
}, ensure_ascii=False, indent=2))
PY

  create_index

  find "$REPORT_DIR" \
    -type f \
    ! -name SHA256SUMS \
    -print0 |
    sort -z |
    xargs -0 sha256sum \
    > "$REPORT_DIR/SHA256SUMS"
}

fatal() {
  local reason="$1"
  local rc="${2:-1}"
  local line="${3:-$LINENO}"

  trap - ERR
  write_stop "$reason" "$rc" "$line"
  print_links
  exit "$rc"
}

on_error() {
  local rc="$?"
  local line="$1"
  fatal "UNEXPECTED_ERROR" "$rc" "$line"
}

trap 'on_error "$LINENO"' ERR

stage "01/05" "Проверяю D2 и сохраняю его артефакты"

for required in \
  "$D2_DIR/report.txt" \
  "$D2_DIR/facts.json" \
  "$D2_DIR/runtime-baseline.json" \
  "$D2_DIR/scheduler-contract.txt" \
  "$D2_DIR/evidence-index.json"
do
  [ -s "$required" ] || {
    echo "MISSING_REQUIRED=$required"
    fatal "D2_REQUIRED_ARTIFACT_MISSING" 2 "$LINENO"
  }
done

grep -Fq \
  "decision=PASS_STEP_050M07D2_FREEZE_PRE0420_EVIDENCE" \
  "$D2_DIR/report.txt" ||
  fatal "D2_PASS_NOT_PROVEN" 3 "$LINENO"

grep -Fq \
  "exact_cron_0420_found=true" \
  "$D2_DIR/report.txt" ||
  fatal "D2_SCHEDULE_NOT_PROVEN" 4 "$LINENO"

cp -a "$D2_DIR/facts.json" \
  "$REPORT_DIR/sources/d2-facts.json"

cp -a "$D2_DIR/runtime-baseline.json" \
  "$REPORT_DIR/sources/d2-runtime-baseline.json"

cp -a "$D2_DIR/scheduler-contract.txt" \
  "$REPORT_DIR/sources/d2-scheduler-contract.txt"

cp -a "$D2_DIR/evidence-index.json" \
  "$REPORT_DIR/sources/d2-evidence-index.json"

mark_success "d2_artifacts_preserved"

stage "02/05" "Публикую точный BusyBox-compatible extractor"

cat > "$REPORT_DIR/vm101.sh" <<'VM101'
#!/bin/sh
set -u
umask 077

RUNNER="/usr/local/sbin/router-egress-emergency-refresh.sh"
PLANNER="/usr/local/sbin/router-egress-hmn-plan-top5.sh"
HELPER="/usr/local/lib/router-egress-recovery-state.sh"
CONF="/etc/router-egress-emergency-refresh.conf"
POOL="/root/hmn/cache/ok-awg1-strict-foreign-latest.tsv"

HOOK_INIT="/etc/init.d/router-egress-emergency-decision"
WATCHER_INIT="/etc/init.d/router-egress-health-repair"

EXPECTED_CRON='/root/hmn/hmn-refresh-pool-safe.sh'

fact() {
  printf '__FACT__ %s=%s\n' "$1" "$2"
}

block() {
  name="$1"
  shift

  echo "__BLOCK_BEGIN__ $name"
  "$@" 2>&1 || true
  echo "__BLOCK_END__ $name"
}

bool_cmd() {
  if "$@" >/dev/null 2>&1; then
    printf true
  else
    printf false
  fi
}

strict_iface() {
  interface="$1"
  attempt=1

  while [ "$attempt" -le 3 ]; do
    if ping \
      -I "$interface" \
      -c 1 \
      -W 3 \
      1.1.1.1 \
      >/dev/null 2>&1
    then
      return 0
    fi

    attempt=$((attempt + 1))
    sleep 1
  done

  return 1
}

routes_all() {
  for table in 201 202 203 204 205; do
    ip route show table "$table" 2>/dev/null |
      grep -q '^default ' ||
      return 1
  done

  return 0
}

state_value() {
  key="$1"
  fallback="$2"

  (
    unset REG_STATE_DIR
    . "$HELPER"
    reg_get_state "$key" "$fallback"
  )
}

repair_counter() {
  (
    unset REG_STATE_DIR
    . "$HELPER"
    reg_daily_repair_get
  )
}

for required in \
  "$RUNNER" \
  "$PLANNER" \
  "$HELPER" \
  "$CONF" \
  "$POOL"
do
  [ -f "$required" ] || {
    echo "__ERROR__ source_missing=$required"
    exit 21
  }
done

echo "__TRACE__ stage=clock"

SNAPSHOT_EPOCH="$(date +%s)"

fact snapshot_epoch "$SNAPSHOT_EPOCH"
fact vm101_local_time "$(date '+%Y-%m-%dT%H:%M:%S%z')"
fact vm101_utc_time "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"

fact etc_tz "$(
  tr '\r\n ' '_' < /etc/TZ 2>/dev/null ||
  echo UNAVAILABLE
)"

fact uci_timezone "$(
  uci -q get system.@system[0].timezone 2>/dev/null ||
  echo UNAVAILABLE
)"

echo "__TRACE__ stage=schedule"

CRON_LINE="$(
  grep -E \
    '^[[:space:]]*20[[:space:]]+4[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+' \
    /etc/crontabs/root \
    2>/dev/null |
  grep -F "$EXPECTED_CRON" |
  head -n1 ||
  true
)"

[ -n "$CRON_LINE" ] || {
  echo "__ERROR__ schedule_missing=20_4_daily_hmn_refresh"
  exit 22
}

fact cron_0420_found true
fact cron_line_sha256 "$(
  printf '%s\n' "$CRON_LINE" |
    sha256sum |
    sed 's/[[:space:]].*$//'
)"

echo "__BLOCK_BEGIN__ exact_cron_line"
printf '%s\n' "$CRON_LINE"
echo "__BLOCK_END__ exact_cron_line"

echo "__TRACE__ stage=endpoints"

HEALTHY_COUNT=0
ENDPOINT_COUNT=0

for interface in vpn1 vpn2 vpn3 vpn4 vpn5; do
  if strict_iface "$interface"; then
    strict=true
    HEALTHY_COUNT=$((HEALTHY_COUNT + 1))
  else
    strict=false
  fi

  fact "strict.${interface}" "$strict"

  ENDPOINT_LINE="$(
    wg show "$interface" endpoints 2>/dev/null |
      head -n1 ||
    true
  )"

  ENDPOINT="$(
    printf '%s\n' "$ENDPOINT_LINE" |
      cut -f2 |
      tr -d '\r\n'
  )"

  if [ -z "$ENDPOINT" ] ||
     [ "$ENDPOINT" = "(none)" ]
  then
    echo "__ERROR__ endpoint_unresolved=$interface"
    exit 23
  fi

  ENDPOINT_COUNT=$((ENDPOINT_COUNT + 1))

  fact "endpoint.${interface}" "$ENDPOINT"

  DUMP_LINE="$(
    wg show "$interface" dump 2>/dev/null |
      sed -n '2p'
  )"

  PUBLIC_KEY="$(
    printf '%s\n' "$DUMP_LINE" |
      cut -f1
  )"

  HANDSHAKE="$(
    printf '%s\n' "$DUMP_LINE" |
      cut -f5
  )"

  RX_BYTES="$(
    printf '%s\n' "$DUMP_LINE" |
      cut -f6
  )"

  TX_BYTES="$(
    printf '%s\n' "$DUMP_LINE" |
      cut -f7
  )"

  fact "public_key_sha256.${interface}" "$(
    printf '%s' "$PUBLIC_KEY" |
      sha256sum |
      sed 's/[[:space:]].*$//'
  )"

  fact "latest_handshake.${interface}" "${HANDSHAKE:-0}"
  fact "rx_bytes.${interface}" "${RX_BYTES:-0}"
  fact "tx_bytes.${interface}" "${TX_BYTES:-0}"
done

fact endpoint_count "$ENDPOINT_COUNT"
fact healthy_vpn_slots "$HEALTHY_COUNT"
fact strict_all "$(
  if [ "$HEALTHY_COUNT" -eq 5 ]; then
    echo true
  else
    echo false
  fi
)"

fact routes_201_205 "$(bool_cmd routes_all)"

echo "__TRACE__ stage=runtime"

fact hook_running "$(bool_cmd "$HOOK_INIT" running)"
fact hook_enabled "$(bool_cmd "$HOOK_INIT" enabled)"
fact watcher_running "$(bool_cmd "$WATCHER_INIT" running)"
fact watcher_enabled "$(bool_cmd "$WATCHER_INIT" enabled)"

fact emergency_lock_present "$(
  bool_cmd test -e /var/lock/router-egress-emergency-refresh.lock
)"

fact refresh_lock_present "$(
  bool_cmd test -e /tmp/hmn-refresh-pool-safe.lock
)"

fact state_mode "$(state_value mode UNKNOWN)"
fact state_status "$(
  state_value last_emergency_refresh_status UNKNOWN
)"
fact state_epoch "$(
  state_value last_emergency_refresh_epoch 0
)"
fact repair_counter "$(repair_counter)"

fact commit_raw "$(
  (
    . "$CONF"
    printf '%s' "${EMERGENCY_COMMIT_ENABLED:-UNSET}"
  )
)"

fact pool_sha256 "$(
  sha256sum "$POOL" |
    sed 's/[[:space:]].*$//'
)"

fact pool_rows "$(
  sed '1d' "$POOL" |
    grep -c . ||
  true
)"

fact pool_mtime_epoch "$(date -r "$POOL" +%s)"

LATEST_GENERATION="$(
  readlink -f /root/hmn/configs/awg1/latest 2>/dev/null ||
  true
)"

[ -n "$LATEST_GENERATION" ] ||
  LATEST_GENERATION="UNRESOLVED"

fact latest_generation "$LATEST_GENERATION"

block runner_dry_run "$RUNNER" --dry-run
block current_planner "$PLANNER"

block routes sh -c '
  for table in 200 201 202 203 204 205; do
    echo "===== table $table ====="
    ip route show table "$table" 2>/dev/null || true
  done
'

block cron_log_tail sh -c '
  tail -n 300 \
    /root/hmn/logs/hmn-refresh-pool-cron.log \
    2>/dev/null ||
  true
'

block emergency_log_tail sh -c '
  tail -n 300 \
    /var/log/router-egress-emergency-refresh.log \
    2>/dev/null ||
  true
'

fact read_only true
fact refresh_ran false
fact rebalance_ran false
fact network_changed false
fact services_changed false
fact state_changed false
fact timer_changed false
fact plan_changed false
fact direct_failopen_changed false

echo "__TRACE__ stage=complete"

[ "$ENDPOINT_COUNT" -eq 5 ] || exit 31
[ "$HEALTHY_COUNT" -eq 5 ] || exit 32
[ "$(bool_cmd routes_all)" = true ] || exit 33

exit 0
VM101

chmod 600 "$REPORT_DIR/vm101.sh"
sh -n "$REPORT_DIR/vm101.sh"

mark_success "busybox_compatible_extractor_published"

stage "03/05" "Снимаю точные endpoints и окончательный baseline"

if ssh pve-mgts \
  "ssh \
    -o BatchMode=yes \
    -o ConnectTimeout=8 \
    -o ServerAliveInterval=20 \
    -o ServerAliveCountMax=6 \
    -o StrictHostKeyChecking=no \
    -o UserKnownHostsFile=/dev/null \
    -i /root/.ssh/pve_to_openwrt_mgts_ed25519 \
    root@10.71.100.2 \
    'sh -s'" \
  < "$REPORT_DIR/vm101.sh" \
  > >(tee "$REPORT_DIR/vm101.txt") \
  2> >(tee "$REPORT_DIR/vm101.stderr" >&2)
then
  VM101_RC=0
else
  VM101_RC=$?
fi

echo "vm101_rc=$VM101_RC" |
  tee -a "$PROGRESS_LOG"

[ "$VM101_RC" -eq 0 ] ||
  fatal "VM101_READONLY_SNAPSHOT_FAILED" "$VM101_RC" "$LINENO"

grep -Fq \
  "__TRACE__ stage=complete" \
  "$REPORT_DIR/vm101.txt" ||
  fatal "REMOTE_COMPLETION_MARKER_MISSING" 5 "$LINENO"

mark_success "exact_vm101_snapshot_complete"

stage "04/05" "Строю окончательную точку сравнения"

python3 - \
  "$REPORT_DIR/vm101.txt" \
  "$REPORT_DIR/vm101.stderr" \
  "$REPORT_DIR" <<'PY'
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from zoneinfo import ZoneInfo

runtime_path, stderr_path, report_dir = sys.argv[1:]
root = Path(report_dir)

runtime = Path(runtime_path).read_text(
    encoding="utf-8",
    errors="replace",
)

stderr = Path(stderr_path).read_text(
    encoding="utf-8",
    errors="replace",
)

facts = {}

for line in runtime.splitlines():
    if not line.startswith("__FACT__ "):
        continue

    payload = line[len("__FACT__ "):]

    if "=" in payload:
        key, value = payload.split("=", 1)
        facts[key] = value

endpoints = {
    interface: facts[f"endpoint.{interface}"]
    for interface in (
        "vpn1",
        "vpn2",
        "vpn3",
        "vpn4",
        "vpn5",
    )
}

strict = {
    interface:
        facts[f"strict.{interface}"] == "true"
    for interface in endpoints
}

handshakes = {
    interface:
        int(facts[f"latest_handshake.{interface}"])
    for interface in endpoints
}

traffic = {
    interface: {
        "rx_bytes":
            int(facts[f"rx_bytes.{interface}"]),
        "tx_bytes":
            int(facts[f"tx_bytes.{interface}"]),
    }
    for interface in endpoints
}

snapshot_epoch = int(facts["snapshot_epoch"])

snapshot_utc = datetime.fromtimestamp(
    snapshot_epoch,
    tz=timezone.utc,
)

next_run_utc = snapshot_utc.replace(
    hour=4,
    minute=20,
    second=0,
    microsecond=0,
)

if next_run_utc <= snapshot_utc:
    from datetime import timedelta
    next_run_utc += timedelta(days=1)

amsterdam = ZoneInfo("Europe/Amsterdam")
next_run_amsterdam = next_run_utc.astimezone(amsterdam)

unexpected_stderr = [
    line
    for line in stderr.splitlines()
    if line.strip()
    and not line.startswith("Warning: Permanently added ")
]

checks = {
    "five_endpoints_resolved":
        len(endpoints) == 5
        and all(
            value
            and value != "UNRESOLVED"
            and ":" in value
            for value in endpoints.values()
        ),

    "five_strict_slots":
        all(strict.values())
        and facts["healthy_vpn_slots"] == "5"
        and facts["strict_all"] == "true",

    "routes_201_205":
        facts["routes_201_205"] == "true",

    "services_running_enabled":
        facts["hook_running"] == "true"
        and facts["hook_enabled"] == "true"
        and facts["watcher_running"] == "true"
        and facts["watcher_enabled"] == "true",

    "locks_absent":
        facts["emergency_lock_present"] == "false"
        and facts["refresh_lock_present"] == "false",

    "cron_0420_utc":
        facts["cron_0420_found"] == "true",

    "read_only":
        facts["read_only"] == "true"
        and facts["refresh_ran"] == "false"
        and facts["rebalance_ran"] == "false"
        and facts["network_changed"] == "false"
        and facts["services_changed"] == "false"
        and facts["state_changed"] == "false"
        and facts["timer_changed"] == "false"
        and facts["plan_changed"] == "false"
        and facts["direct_failopen_changed"] == "false",

    "stderr_clean":
        not unexpected_stderr,
}

all_ok = all(checks.values())

baseline = {
    "captured_at": {
        "epoch": snapshot_epoch,
        "vm101_local":
            facts["vm101_local_time"],
        "vm101_utc":
            facts["vm101_utc_time"],
    },
    "schedule": {
        "cron": "20 4 * * *",
        "timezone": "UTC",
        "next_run_utc":
            next_run_utc.isoformat(),
        "next_run_amsterdam":
            next_run_amsterdam.isoformat(),
    },
    "endpoints": endpoints,
    "strict": strict,
    "handshakes": handshakes,
    "traffic": traffic,
    "healthy_vpn_slots":
        int(facts["healthy_vpn_slots"]),
    "routes_201_205":
        facts["routes_201_205"] == "true",
    "services": {
        "hook_running":
            facts["hook_running"] == "true",
        "hook_enabled":
            facts["hook_enabled"] == "true",
        "watcher_running":
            facts["watcher_running"] == "true",
        "watcher_enabled":
            facts["watcher_enabled"] == "true",
    },
    "locks": {
        "emergency":
            facts["emergency_lock_present"] == "true",
        "refresh":
            facts["refresh_lock_present"] == "true",
    },
    "state": {
        "mode": facts["state_mode"],
        "status": facts["state_status"],
        "epoch": int(facts["state_epoch"]),
        "repair_counter":
            int(facts["repair_counter"]),
        "commit_raw":
            facts["commit_raw"],
    },
    "pool": {
        "sha256": facts["pool_sha256"],
        "rows": int(facts["pool_rows"]),
        "mtime_epoch":
            int(facts["pool_mtime_epoch"]),
        "latest_generation":
            facts["latest_generation"],
    },
    "checks": checks,
}

comparison_contract = {
    "schema": "post-scheduled-refresh-comparison-v1",
    "reference_file": "exact-baseline.json",
    "compare_after": {
        "utc": next_run_utc.isoformat(),
        "amsterdam":
            next_run_amsterdam.isoformat(),
    },
    "required_comparisons": [
        "endpoint changes vpn1..vpn5",
        "strict status vpn1..vpn5",
        "latest handshake and traffic counters",
        "pool sha256, rows and mtime",
        "latest HMN generation",
        "state mode, status and epoch",
        "repair counter",
        "cron log delta",
        "emergency log delta",
        "planner decision and changes_count",
        "presence or absence of commit_failed",
        "presence or absence of slot_apply_failed_egress1",
    ],
    "do_not_mutate_before_comparison": True,
}

assessment = {
    "all_ok": all_ok,
    "decision": (
        "PASS_STEP_050M07D3_FREEZE_EXACT_ENDPOINTS_BEFORE_SCHEDULED_REFRESH"
        if all_ok
        else
        "STOP_STEP_050M07D3_FREEZE_EXACT_ENDPOINTS_BEFORE_SCHEDULED_REFRESH"
    ),
    "checks": checks,
    "failed_checks": [
        name
        for name, value in checks.items()
        if not value
    ],
    "exact_endpoints_frozen": all_ok,
    "schedule": baseline["schedule"],
    "safety": {
        "read_only": True,
        "refresh_ran": False,
        "rebalance_ran": False,
        "network_changed": False,
        "services_changed": False,
        "state_changed": False,
        "timer_changed": False,
        "plan_changed": False,
        "direct_failopen_changed": False,
    },
    "next_action":
        "Pause. Run read-only comparison after scheduled refresh.",
}

(root / "exact-baseline.json").write_text(
    json.dumps(
        baseline,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

(root / "comparison-contract.json").write_text(
    json.dumps(
        comparison_contract,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

(root / "assessment.json").write_text(
    json.dumps(
        assessment,
        ensure_ascii=False,
        indent=2,
    ) + "\n",
    encoding="utf-8",
)

if not all_ok:
    raise SystemExit(
        "failed checks: "
        + ",".join(assessment["failed_checks"])
    )
PY

mark_success "exact_baseline_built"

stage "05/05" "Публикую окончательный pre-refresh freeze"

NEXT_UTC="$(
  python3 - "$REPORT_DIR/exact-baseline.json" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as source:
    data = json.load(source)

print(data["schedule"]["next_run_utc"])
PY
)"

NEXT_AMSTERDAM="$(
  python3 - "$REPORT_DIR/exact-baseline.json" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as source:
    data = json.load(source)

print(data["schedule"]["next_run_amsterdam"])
PY
)"

ENDPOINT_SUMMARY="$(
  python3 - "$REPORT_DIR/exact-baseline.json" <<'PY'
import json
import sys

with open(sys.argv[1], encoding="utf-8") as source:
    data = json.load(source)

print(
    ", ".join(
        f"{name}={endpoint}"
        for name, endpoint
        in data["endpoints"].items()
    )
)
PY
)"

cat > "$REPORT_DIR/report.txt" <<EOF
=== ${STEP} RESULT ===
step=${STEP}
decision=${PASS_DECISION}
all_ok=true
mode=M07_EXACT_PRE_SCHEDULED_REFRESH_FREEZE

schedule:
  cron=20 4 * * *
  timezone=UTC
  next_run_utc=${NEXT_UTC}
  next_run_amsterdam=${NEXT_AMSTERDAM}

baseline:
  exact_endpoints_frozen=true
  endpoints=${ENDPOINT_SUMMARY}
  healthy_vpn_slots=5
  five_strict_slots=true
  routes_201_205=true
  services_running_enabled=true
  locks_absent=true
  details=exact-baseline.json

comparison:
  contract=comparison-contract.json
  next_action=READONLY_POST_SCHEDULED_REFRESH_COMPARISON
  changes_before_comparison=false

safety:
  read_only=true
  refresh_ran=false
  rebalance_ran=false
  network_changed=false
  services_changed=false
  state_changed=false
  timer_changed=false
  plan_changed=false
  direct_failopen_changed=false

plan:
  current_milestone=M07
  milestone_completed=false
  milestone_changed=false

TRYCF_REPORT=${TRYCF_REPORT}
REPORT_TXT=${REPORT_TXT}
FACTS_JSON=${FACTS_JSON}
ARCHITECTURE_PLAN=${ARCHITECTURE_PLAN}
XS_MAP=${XS_MAP}
GLOBAL_PROJECT_PLAN=${GLOBAL_PROJECT_PLAN}
EOF

python3 - \
  "$REPORT_DIR/assessment.json" \
  "$REPORT_DIR/exact-baseline.json" \
  "$STEP" \
  "$TS" \
  "$TRYCF_REPORT" \
  "$REPORT_TXT" \
  "$FACTS_JSON" \
  "$ARCHITECTURE_PLAN" \
  "$XS_MAP" \
  "$GLOBAL_PROJECT_PLAN" \
  > "$REPORT_DIR/facts.json" <<'PY'
import json
import sys

(
    assessment_path,
    baseline_path,
    step,
    timestamp,
    report,
    report_txt,
    facts_json,
    architecture,
    xs_map,
    global_plan,
) = sys.argv[1:]

with open(assessment_path, encoding="utf-8") as source:
    assessment = json.load(source)

with open(baseline_path, encoding="utf-8") as source:
    baseline = json.load(source)

print(json.dumps({
    "schema": "router-step-facts-v1",
    "step": step,
    "generated_at_utc": timestamp,
    "assessment": assessment,
    "exact_baseline": baseline,
    "operation": {
        "exact_endpoints_frozen": True,
        "schedule_frozen": True,
        "comparison_contract_created": True,
    },
    "safety": assessment["safety"],
    "plan": {
        "current_milestone": "M07",
        "milestone_completed": False,
        "milestone_changed": False,
    },
    "next_step":
        "READONLY_POST_SCHEDULED_REFRESH_COMPARISON",
    "publish": {
        "trycf_report": report,
        "report_txt": report_txt,
        "facts_json": facts_json,
        "architecture_plan": architecture,
        "xs_map": xs_map,
        "global_project_plan": global_plan,
    },
}, ensure_ascii=False, indent=2))
PY

create_index

find "$REPORT_DIR" \
  -type f \
  ! -name SHA256SUMS \
  -print0 |
  sort -z |
  xargs -0 sha256sum \
  > "$REPORT_DIR/SHA256SUMS"

mark_success "exact_freeze_published"
trap - ERR

echo "decision=$PASS_DECISION" |
  tee -a "$PROGRESS_LOG"

echo "exact_endpoints_frozen=true" |
  tee -a "$PROGRESS_LOG"

echo "next_run_utc=$NEXT_UTC" |
  tee -a "$PROGRESS_LOG"

echo "next_run_amsterdam=$NEXT_AMSTERDAM" |
  tee -a "$PROGRESS_LOG"

echo "healthy_vpn_slots=5" |
  tee -a "$PROGRESS_LOG"

echo "current_milestone=M07" |
  tee -a "$PROGRESS_LOG"

echo "next_action=READONLY_POST_SCHEDULED_REFRESH_COMPARISON" |
  tee -a "$PROGRESS_LOG"

echo "read_only=true" |
  tee -a "$PROGRESS_LOG"

print_links
