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

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

STEP="STEP_050M07D6_POST_SCHEDULED_REFRESH_READONLY_COMPARISON"
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}"

D5_DIR="${PUBROOT}/20260711-212723_step050m07d5_final_cli_discovery_and_pre_night_freeze"

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}_step050m07d6_post_scheduled_refresh_readonly_comparison"
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>

<h2>Сравнение</h2>
<ul>
<li><a href="report.txt">report.txt</a></li>
<li><a href="facts.json">facts.json</a></li>
<li><a href="assessment.json">assessment.json</a></li>
<li><a href="comparison.json">comparison.json</a></li>
<li><a href="current-baseline.json">current-baseline.json</a></li>
</ul>

<h2>Raw snapshot</h2>
<ul>
<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>
</ul>

<h2>Ночной эталон</h2>
<ul>
<li><a href="sources/d5-facts.json">D5 facts</a></li>
<li><a href="sources/d5-exact-baseline.json">D5 exact baseline</a></li>
<li><a href="sources/d5-comparison-contract.json">D5 comparison contract</a></li>
</ul>
</body>
</html>
EOF
}

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

  cat > "$REPORT_DIR/report.txt" <<EOF
=== ${STEP} RESULT ===
step=${STEP}
decision=STOP_${STEP}_${reason}
all_ok=false
mode=M07_POST_SCHEDULED_REFRESH_READONLY_COMPARISON
error_rc=${rc}
error_line=${line}
failed_stage=${CURRENT_STAGE}
last_success=${LAST_SUCCESS}
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 - \
    "$STEP" \
    "$reason" \
    "$rc" \
    "$line" \
    "$CURRENT_STAGE" \
    "$LAST_SUCCESS" \
    "$VM101_RC" \
    "$TRYCF_REPORT" \
    "$REPORT_TXT" \
    "$FACTS_JSON" \
    "$ARCHITECTURE_PLAN" \
    "$XS_MAP" \
    "$GLOBAL_PROJECT_PLAN" \
    > "$REPORT_DIR/facts.json" <<'PY'
import json
import sys

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

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,
        "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,
    },
    "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"

  print_links
  exit "$rc"
}

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

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

trap 'rc=$?; fatal "UNEXPECTED_ERROR" "$rc" "$LINENO"' ERR

stage "01/05" "Проверяю ночной checkpoint D5"

for required in \
  "$D5_DIR/report.txt" \
  "$D5_DIR/facts.json" \
  "$D5_DIR/exact-baseline.json" \
  "$D5_DIR/comparison-contract.json"
do
  [ -s "$required" ] || {
    echo "MISSING_REQUIRED=$required"
    fatal "D5_REQUIRED_ARTIFACT_MISSING" 2 "$LINENO"
  }
done

grep -Fq \
  "decision=PASS_STEP_050M07D5_FINAL_CLI_DISCOVERY_AND_PRE_NIGHT_FREEZE" \
  "$D5_DIR/report.txt" ||
  fatal "D5_PASS_NOT_PROVEN" 3 "$LINENO"

cp -a "$D5_DIR/facts.json" \
  "$REPORT_DIR/sources/d5-facts.json"

cp -a "$D5_DIR/exact-baseline.json" \
  "$REPORT_DIR/sources/d5-exact-baseline.json"

cp -a "$D5_DIR/comparison-contract.json" \
  "$REPORT_DIR/sources/d5-comparison-contract.json"

mark_success "d5_baseline_loaded"

stage "02/05" "Публикую read-only snapshot VM101"

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"

CRON_LOG="/root/hmn/logs/hmn-refresh-pool-cron.log"
EMERGENCY_LOG="/var/log/router-egress-emergency-refresh.log"

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

json_block() {
  echo "__JSON_BEGIN__ $1"
  printf '%s\n' "$2"
  echo "__JSON_END__ $1"
}

block_file_tail() {
  name="$1"
  path="$2"
  lines="$3"

  echo "__BLOCK_BEGIN__ $name"

  if [ -f "$path" ]; then
    tail -n "$lines" "$path" 2>/dev/null || true
  else
    echo "FILE_NOT_PRESENT=$path"
  fi

  echo "__BLOCK_END__ $name"
}

block_cmd() {
  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_and_schedule"

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

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

fact cron_0420_found "$(
  if [ -n "$CRON_LINE" ]; then
    echo true
  else
    echo false
  fi
)"

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

echo "__TRACE__ stage=health"

HEALTHY_COUNT=0

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

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

fact healthy_vpn_slots "$HEALTHY_COUNT"

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

fact routes_all "$(bool_cmd routes_all)"

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
)"

echo "__TRACE__ stage=planner_and_runtime"

PLANNER_JSON="$("$PLANNER")"
RUNNER_JSON="$("$RUNNER" --dry-run)"

json_block planner "$PLANNER_JSON"
json_block runner "$RUNNER_JSON"

AMNEZIAWG_BIN="$(
  command -v amneziawg 2>/dev/null ||
  true
)"

fact amneziawg_binary "$(
  if [ -n "$AMNEZIAWG_BIN" ]; then
    printf '%s' "$AMNEZIAWG_BIN"
  else
    printf 'NOT_FOUND'
  fi
)"

if [ -n "$AMNEZIAWG_BIN" ]; then
  echo "__BLOCK_BEGIN__ amneziawg_all_dump"
  "$AMNEZIAWG_BIN" show all dump 2>&1 || true
  echo "__BLOCK_END__ amneziawg_all_dump"

  echo "__BLOCK_BEGIN__ amneziawg_interfaces"

  for interface in vpn1 vpn2 vpn3 vpn4 vpn5; do
    echo "===== $interface ====="
    "$AMNEZIAWG_BIN" show "$interface" 2>&1 || true
  done

  echo "__BLOCK_END__ amneziawg_interfaces"
fi

block_cmd routes_200_205 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
'

echo "__TRACE__ stage=state_and_pool"

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"

echo "__TRACE__ stage=logs"

block_file_tail cron_log_tail "$CRON_LOG" 1200
block_file_tail emergency_log_tail "$EMERGENCY_LOG" 1200

block_cmd recent_hmn_files sh -c '
  find /root/hmn \
    -maxdepth 4 \
    -type f \
    -mmin -720 \
    -exec ls -l {} \; \
    2>/dev/null |
  sort
'

block_cmd filesystem_usage df -Pk

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"
exit 0
VM101

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

mark_success "readonly_snapshot_script_published"

stage "03/05" "Снимаю текущее состояние после scheduled refresh"

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 "post_schedule_snapshot_complete"

stage "04/05" "Сравниваю состояние с D5"

python3 - \
  "$D5_DIR/exact-baseline.json" \
  "$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

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

root = Path(report_dir)

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

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

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

facts = {}
blocks = {}
current_block = None
block_lines = []

for line in runtime.splitlines():
    if line.startswith("__FACT__ "):
        payload = line[len("__FACT__ "):]

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

    elif line.startswith("__JSON_BEGIN__ "):
        current_block = line[len("__JSON_BEGIN__ "):]
        block_lines = []

    elif line.startswith("__JSON_END__ "):
        name = line[len("__JSON_END__ "):]

        if name == current_block:
            blocks[name] = json.loads(
                "\n".join(block_lines)
            )

        current_block = None
        block_lines = []

    elif current_block is not None:
        block_lines.append(line)


def get_text_block(name: str) -> str:
    pattern = re.compile(
        rf"^__BLOCK_BEGIN__ {re.escape(name)}\n"
        rf"(.*?)"
        rf"^__BLOCK_END__ {re.escape(name)}$",
        re.MULTILINE | re.DOTALL,
    )

    match = pattern.search(runtime)
    return match.group(1) if match else ""


planner = blocks["planner"]
runner = blocks["runner"]

interfaces = [
    "vpn1",
    "vpn2",
    "vpn3",
    "vpn4",
    "vpn5",
]

current_endpoints = {
    item["iface"]: item.get("current")
    for item in planner.get("plan", [])
    if item.get("iface") in interfaces
}

target_endpoints = {
    item["iface"]: item.get("target")
    for item in planner.get("plan", [])
    if item.get("iface") in interfaces
}

before_endpoints = before["endpoints"]

endpoint_changes = {
    interface: {
        "before": before_endpoints.get(interface),
        "after": current_endpoints.get(interface),
        "changed":
            before_endpoints.get(interface)
            != current_endpoints.get(interface),
    }
    for interface in interfaces
}

changed_interfaces = [
    interface
    for interface, data in endpoint_changes.items()
    if data["changed"]
]

strict_after = {
    interface:
        facts.get(f"strict.{interface}") == "true"
    for interface in interfaces
}

scheduled_utc = datetime.fromisoformat(
    before["schedule"]["next_run_utc"]
)

scheduled_epoch = int(
    scheduled_utc.timestamp()
)

snapshot_epoch = int(facts["snapshot_epoch"])

pool_before = before["pool"]
pool_after = {
    "sha256": facts["pool_sha256"],
    "rows": int(facts["pool_rows"]),
    "mtime_epoch":
        int(facts["pool_mtime_epoch"]),
    "latest_generation":
        facts["latest_generation"],
}

pool_changed = (
    pool_before["sha256"] != pool_after["sha256"]
    or pool_before["mtime_epoch"]
        != pool_after["mtime_epoch"]
    or pool_before["latest_generation"]
        != pool_after["latest_generation"]
)

generation_changed = (
    pool_before["latest_generation"]
    != pool_after["latest_generation"]
)

pool_written_after_schedule = (
    pool_after["mtime_epoch"]
    >= scheduled_epoch
)

cron_log = get_text_block("cron_log_tail")
emergency_log = get_text_block(
    "emergency_log_tail"
)

combined_logs = cron_log + "\n" + emergency_log

schedule_date = scheduled_utc.strftime("%Y-%m-%d")

scheduled_log_lines = [
    line
    for line in combined_logs.splitlines()
    if schedule_date in line
]

refresh_log_evidence = [
    line
    for line in scheduled_log_lines
    if re.search(
        r"refresh|serverlist|download|validate|manager|done|start",
        line,
        re.IGNORECASE,
    )
]

failure_log_evidence = [
    line
    for line in scheduled_log_lines
    if re.search(
        r"commit_failed|slot_apply_failed|error|failed|rollback",
        line,
        re.IGNORECASE,
    )
]

egress1_failure_lines = [
    line
    for line in failure_log_evidence
    if re.search(
        r"slot_apply_failed_egress1|egress1",
        line,
        re.IGNORECASE,
    )
]

scheduled_refresh_observed = (
    snapshot_epoch >= scheduled_epoch
    and pool_changed
    and generation_changed
    and pool_written_after_schedule
)

health_checks = {
    "five_current_endpoints":
        set(current_endpoints) == set(interfaces)
        and all(current_endpoints.values()),

    "five_strict_slots":
        all(strict_after.values())
        and facts.get("healthy_vpn_slots") == "5"
        and facts.get("strict_all") == "true",

    "routes_201_205":
        facts.get("routes_all") == "true",

    "services_running_enabled":
        facts.get("hook_running") == "true"
        and facts.get("hook_enabled") == "true"
        and facts.get("watcher_running") == "true"
        and facts.get("watcher_enabled") == "true",

    "locks_absent":
        facts.get("emergency_lock_present") == "false"
        and facts.get("refresh_lock_present") == "false",

    "cron_still_present":
        facts.get("cron_0420_found") == "true",

    "read_only_snapshot":
        facts.get("read_only") == "true"
        and facts.get("refresh_ran") == "false"
        and facts.get("rebalance_ran") == "false"
        and facts.get("network_changed") == "false"
        and facts.get("services_changed") == "false"
        and facts.get("state_changed") == "false"
        and facts.get("timer_changed") == "false"
        and facts.get("plan_changed") == "false"
        and facts.get("direct_failopen_changed") == "false",
}

baseline_healthy = all(health_checks.values())

planner_changes_before = int(
    before["planner"]["changes_count"]
)

planner_changes_after = int(
    planner.get("changes_count", -1)
)

state_before = before["state"]

state_after = {
    "mode": facts["state_mode"],
    "status": facts["state_status"],
    "epoch": int(facts["state_epoch"]),
    "repair_counter":
        int(facts["repair_counter"]),
    "commit_raw": facts["commit_raw"],
}

state_changes = {
    key: {
        "before": state_before.get(key),
        "after": state_after.get(key),
        "changed":
            state_before.get(key)
            != state_after.get(key),
    }
    for key in state_after
}

if not baseline_healthy:
    classification = (
        "POST_SCHEDULE_BASELINE_DEGRADED"
    )

elif not scheduled_refresh_observed:
    if refresh_log_evidence:
        classification = (
            "SCHEDULED_REFRESH_STARTED_"
            "BUT_POOL_UPDATE_NOT_PROVEN"
        )
    else:
        classification = (
            "SCHEDULED_REFRESH_NOT_PROVEN"
        )

elif egress1_failure_lines:
    classification = (
        "SCHEDULED_REFRESH_COMPLETED_"
        "WITH_EGRESS1_FAILURE_MARKERS"
    )

elif failure_log_evidence:
    classification = (
        "SCHEDULED_REFRESH_COMPLETED_"
        "WITH_FAILURE_MARKERS"
    )

elif changed_interfaces and planner_changes_after == 0:
    classification = (
        "SCHEDULED_REFRESH_REBALANCED_"
        "AND_CONVERGED"
    )

elif changed_interfaces:
    classification = (
        "SCHEDULED_REFRESH_CHANGED_ENDPOINTS_"
        "PLANNER_NOT_CONVERGED"
    )

elif planner_changes_after == planner_changes_before:
    classification = (
        "POOL_REFRESHED_LIVE_ENDPOINTS_UNCHANGED"
    )

else:
    classification = (
        "POOL_REFRESHED_WITH_PARTIAL_PLANNER_CHANGE"
    )

comparison = {
    "schedule": {
        "expected_run_utc":
            before["schedule"]["next_run_utc"],
        "expected_run_amsterdam":
            before["schedule"]["next_run_amsterdam"],
        "snapshot_epoch": snapshot_epoch,
        "snapshot_after_schedule":
            snapshot_epoch >= scheduled_epoch,
    },
    "scheduled_refresh": {
        "observed": scheduled_refresh_observed,
        "pool_written_after_schedule":
            pool_written_after_schedule,
        "refresh_log_evidence":
            refresh_log_evidence[-100:],
        "failure_log_evidence":
            failure_log_evidence[-100:],
        "egress1_failure_evidence":
            egress1_failure_lines[-100:],
    },
    "endpoints": {
        "changes": endpoint_changes,
        "changed_interfaces":
            changed_interfaces,
        "target_after": target_endpoints,
    },
    "pool": {
        "before": pool_before,
        "after": pool_after,
        "changed": pool_changed,
        "generation_changed":
            generation_changed,
    },
    "planner": {
        "before_changes_count":
            planner_changes_before,
        "after_decision":
            planner.get("decision"),
        "after_changes_count":
            planner_changes_after,
        "after_plan":
            planner.get("plan", []),
    },
    "state": {
        "before": state_before,
        "after": state_after,
        "changes": state_changes,
    },
    "health": {
        "checks": health_checks,
        "all_ok": baseline_healthy,
        "strict_after": strict_after,
    },
    "classification": classification,
}

warnings = []

if planner_changes_after > 0:
    warnings.append(
        f"planner_changes_count={planner_changes_after}"
    )

if failure_log_evidence:
    warnings.append(
        "new_failure_markers_detected"
    )

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

if unexpected_stderr:
    warnings.append(
        "unexpected_ssh_or_remote_stderr"
    )

assessment = {
    "all_ok": True,
    "decision":
        "PASS_STEP_050M07D6_POST_SCHEDULED_REFRESH_READONLY_COMPARISON",
    "operational_classification":
        classification,
    "scheduled_refresh_observed":
        scheduled_refresh_observed,
    "baseline_healthy": baseline_healthy,
    "changed_interfaces":
        changed_interfaces,
    "planner_changes_before":
        planner_changes_before,
    "planner_changes_after":
        planner_changes_after,
    "warnings": warnings,
    "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,
    },
}

current_baseline = {
    "captured_at": {
        "epoch": snapshot_epoch,
        "vm101_local":
            facts["vm101_local_time"],
        "vm101_utc":
            facts["vm101_utc_time"],
    },
    "protocol": "AmneziaWG",
    "runtime_cli":
        facts.get("amneziawg_binary"),
    "endpoints": current_endpoints,
    "strict": strict_after,
    "healthy_vpn_slots":
        int(facts["healthy_vpn_slots"]),
    "routes_201_205":
        facts["routes_all"] == "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",
    },
    "pool": pool_after,
    "state": state_after,
    "planner": planner,
    "runner": runner,
}

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

(root / "current-baseline.json").write_text(
    json.dumps(
        current_baseline,
        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",
)
PY

mark_success "post_schedule_comparison_built"

stage "05/05" "Публикую результат сравнения"

CLASSIFICATION="$(
  python3 - "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

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

print(data["operational_classification"])
PY
)"

SCHEDULED_OBSERVED="$(
  python3 - "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

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

print(
    "true"
    if data["scheduled_refresh_observed"]
    else "false"
)
PY
)"

BASELINE_HEALTHY="$(
  python3 - "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

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

print(
    "true"
    if data["baseline_healthy"]
    else "false"
)
PY
)"

CHANGED_INTERFACES="$(
  python3 - "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

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

values = data["changed_interfaces"]
print(",".join(values) if values else "NONE")
PY
)"

PLANNER_BEFORE="$(
  python3 - "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

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

print(data["planner_changes_before"])
PY
)"

PLANNER_AFTER="$(
  python3 - "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

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

print(data["planner_changes_after"])
PY
)"

WARNINGS="$(
  python3 - "$REPORT_DIR/assessment.json" <<'PY'
import json
import sys

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

print(
    ",".join(data["warnings"])
    if data["warnings"]
    else "NONE"
)
PY
)"

ENDPOINT_DELTAS="$(
  python3 - "$REPORT_DIR/comparison.json" <<'PY'
import json
import sys

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

for interface, item in sorted(
    data["endpoints"]["changes"].items()
):
    marker = "CHANGED" if item["changed"] else "SAME"

    print(
        f"{interface}: "
        f"{item['before']} -> {item['after']} "
        f"[{marker}]"
    )
PY
)"

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

operational_classification:
  value=${CLASSIFICATION}
  scheduled_refresh_observed=${SCHEDULED_OBSERVED}
  baseline_healthy=${BASELINE_HEALTHY}

endpoint_changes:
${ENDPOINT_DELTAS}

planner:
  changes_count_before=${PLANNER_BEFORE}
  changes_count_after=${PLANNER_AFTER}

assessment:
  warnings=${WARNINGS}
  comparison=comparison.json
  current_baseline=current-baseline.json
  details=assessment.json

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

next_step:
  CLASSIFY_D6_RESULT_BEFORE_ANY_MUTATION

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/comparison.json" \
  "$REPORT_DIR/current-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,
    comparison_path,
    current_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(comparison_path, encoding="utf-8") as source:
    comparison = json.load(source)

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

print(json.dumps({
    "schema": "router-step-facts-v1",
    "step": step,
    "generated_at_utc": timestamp,
    "assessment": assessment,
    "comparison": comparison,
    "current_baseline": current,
    "safety": assessment["safety"],
    "plan": {
        "current_milestone": "M07",
        "milestone_completed": False,
        "milestone_changed": False,
    },
    "next_step":
        "CLASSIFY_D6_RESULT_BEFORE_ANY_MUTATION",
    "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 "post_schedule_report_published"
trap - ERR

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

echo "operational_classification=$CLASSIFICATION" |
  tee -a "$PROGRESS_LOG"

echo "scheduled_refresh_observed=$SCHEDULED_OBSERVED" |
  tee -a "$PROGRESS_LOG"

echo "baseline_healthy=$BASELINE_HEALTHY" |
  tee -a "$PROGRESS_LOG"

echo "changed_interfaces=$CHANGED_INTERFACES" |
  tee -a "$PROGRESS_LOG"

echo "planner_changes_before=$PLANNER_BEFORE" |
  tee -a "$PROGRESS_LOG"

echo "planner_changes_after=$PLANNER_AFTER" |
  tee -a "$PROGRESS_LOG"

echo "warnings=$WARNINGS" |
  tee -a "$PROGRESS_LOG"

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

print_links
