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

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

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

D4B_DIR="${PUBROOT}/20260711-212435_step050m07d4b_final_selfcontained_pre_night_freeze_awg"
M07D_DIR="${PUBROOT}/20260711-210850_step050m07d_classify_false_success_and_egress1_failure"
D2_DIR="${PUBROOT}/20260711-211307_step050m07d2_freeze_pre0420_evidence"
C5_DIR="${PUBROOT}/20260711-205604_step050m07c5_tmpfs_surgical_backup_fresh_refresh_live"

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}_step050m07d5_final_cli_discovery_and_pre_night_freeze"
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 первым.
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>Финальный checkpoint</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="exact-baseline.json">exact-baseline.json</a></li>
<li><a href="comparison-contract.json">comparison-contract.json</a></li>
<li><a href="cli-discovery.json">cli-discovery.json</a></li>
</ul>

<h2>VM101 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/d4b-facts.json">D4B STOP facts</a></li>
<li><a href="sources/m07d-source-contract.json">M07D source contract</a></li>
<li><a href="sources/m07d-failure-artifacts.txt">M07D failure artifacts</a></li>
<li><a href="sources/d2-scheduler-contract.txt">D2 scheduler contract</a></li>
<li><a href="sources/c5-live-stream.txt">C5 live stream</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: int = 100000):
    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 "planner_contract_invalid=" in combined:
    classification = "LIVE_PLANNER_CONTRACT_INVALID"
elif "strict_all=false" in combined:
    classification = "PRE_NIGHT_VPN_BASELINE_DEGRADED"
elif "routes_all=false" in combined:
    classification = "PRE_NIGHT_ROUTE_BASELINE_DEGRADED"
elif "schedule_missing=" in combined:
    classification = "SCHEDULE_CONTRACT_MISSING"
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-v14",
    "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,
}, 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_FINAL_CLI_DISCOVERY_AND_PRE_NIGHT_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"
}

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

stage "01/05" "Проверяю D4B и сохраняю устойчивый контекст"

for required in \
  "$D4B_DIR/report.txt" \
  "$D4B_DIR/facts.json" \
  "$M07D_DIR/source-contract.json" \
  "$M07D_DIR/failure-artifacts.txt" \
  "$D2_DIR/scheduler-contract.txt" \
  "$C5_DIR/vm101.txt"
do
  [ -s "$required" ] || {
    echo "MISSING_REQUIRED=$required"
    fatal "REQUIRED_ARTIFACT_MISSING" 2 "$LINENO"
  }
done

grep -Fq \
  '"automatic_classification": "AMNEZIAWG_CLI_MISSING"' \
  "$D4B_DIR/facts.json" ||
  fatal "D4B_CLI_FAILURE_NOT_PROVEN" 3 "$LINENO"

grep -Fq \
  '"read_only": true' \
  "$D4B_DIR/facts.json" ||
  fatal "D4B_READONLY_NOT_PROVEN" 4 "$LINENO"

cp -a "$D4B_DIR/facts.json" \
  "$REPORT_DIR/sources/d4b-facts.json"

cp -a "$M07D_DIR/source-contract.json" \
  "$REPORT_DIR/sources/m07d-source-contract.json"

cp -a "$M07D_DIR/failure-artifacts.txt" \
  "$REPORT_DIR/sources/m07d-failure-artifacts.txt"

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

cp -a "$C5_DIR/vm101.txt" \
  "$REPORT_DIR/sources/c5-live-stream.txt"

mark_success "persistent_context_preserved"

stage "02/05" "Публикую read-only discovery и freeze script"

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"

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

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

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=cli_discovery"

for candidate in \
  awg \
  wg \
  amneziawg \
  amnezia-wg \
  amneziawg-go \
  wg-amnezia
do
  path="$(
    command -v "$candidate" 2>/dev/null ||
    true
  )"

  [ -n "$path" ] ||
    path="NOT_FOUND"

  fact "cli.${candidate}" "$path"
done

block matching_executables sh -c '
  for directory in /bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin; do
    [ -d "$directory" ] || continue

    find "$directory" \
      -maxdepth 1 \
      -type f \
      2>/dev/null |
      grep -Ei "/(a?wg|amnezia|wireguard)" ||
    true
  done |
  sort -u
'

block installed_packages sh -c '
  opkg list-installed 2>/dev/null |
    grep -Ei "amnezia|wireguard|(^|-)wg($|-)|kmod.*wg" ||
  true
'

block kernel_modules sh -c '
  lsmod 2>/dev/null |
    grep -Ei "amnezia|wireguard|awg" ||
  true
'

block interface_link_details sh -c '
  for interface in vpn1 vpn2 vpn3 vpn4 vpn5; do
    echo "===== $interface ====="
    ip -details link show dev "$interface" 2>/dev/null || true
  done
'

block network_protocols sh -c '
  uci -q show network 2>/dev/null |
    grep -E \
      "^network\.vpn[1-5]=|^network\.vpn[1-5]\.proto=|wireguard_vpn[1-5]|amnezia" |
    grep -vE \
      "private_key|preshared_key" ||
  true
'

block ubus_interface_status sh -c '
  for interface in vpn1 vpn2 vpn3 vpn4 vpn5; do
    echo "===== $interface ====="
    ubus call "network.interface.$interface" status 2>/dev/null || true
  done
'

for candidate in awg wg amneziawg amnezia-wg wg-amnezia; do
  path="$(
    command -v "$candidate" 2>/dev/null ||
    true
  )"

  [ -n "$path" ] || continue

  echo "__BLOCK_BEGIN__ cli_probe_${candidate}"

  "$path" --version 2>&1 || true
  "$path" show 2>&1 || true

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

  echo "__BLOCK_END__ cli_probe_${candidate}"
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')"

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

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

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

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

fact cron_0420_found true

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

echo "__TRACE__ stage=health_and_routes"

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

block 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=planner_state_and_pool"

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

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

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 cron_log_tail sh -c '
  tail -n 400 \
    /root/hmn/logs/hmn-refresh-pool-cron.log \
    2>/dev/null ||
  true
'

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

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

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

exit 0
VM101

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

mark_success "discovery_and_freeze_script_published"

stage "03/05" "Снимаю окончательный read-only snapshot"

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

stage "04/05" "Строю baseline без предположения о CLI"

python3 - \
  "$REPORT_DIR/vm101.txt" \
  "$REPORT_DIR/vm101.stderr" \
  "$REPORT_DIR" <<'PY'
import json
import re
import sys
from datetime import datetime, timedelta, 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 = {}
blocks = {}
current = None
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 = line[len("__JSON_BEGIN__ "):]
        lines = []

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

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

        current = None
        lines = []

    elif current is not None:
        lines.append(line)

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

expected_ifaces = {
    "vpn1",
    "vpn2",
    "vpn3",
    "vpn4",
    "vpn5",
}

plan = planner.get("plan", [])

observed_ifaces = {
    item.get("iface")
    for item in plan
}

if planner.get("decision") != "plan_ok":
    raise SystemExit(
        "planner_contract_invalid=decision"
    )

if observed_ifaces != expected_ifaces:
    raise SystemExit(
        "planner_contract_invalid=iface_set"
    )

endpoints = {}

for item in plan:
    iface = item["iface"]
    endpoint = item.get("current", "")

    if (
        not endpoint
        or endpoint in {"UNRESOLVED", "(none)"}
        or not re.match(r"^.+:[0-9]+$", endpoint)
    ):
        raise SystemExit(
            f"planner_contract_invalid={iface}:{endpoint}"
        )

    endpoints[iface] = endpoint

cli_candidates = {
    name: facts.get(f"cli.{name}", "NOT_FOUND")
    for name in (
        "awg",
        "wg",
        "amneziawg",
        "amnezia-wg",
        "amneziawg-go",
        "wg-amnezia",
    )
}

available_cli = {
    name: path
    for name, path in cli_candidates.items()
    if path != "NOT_FOUND"
}

strict = {
    iface:
        facts.get(f"strict.{iface}") == "true"
    for iface in sorted(expected_ifaces)
}

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:
    next_run_utc += timedelta(days=1)

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

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

checks = {
    "planner_contract":
        planner.get("decision") == "plan_ok"
        and observed_ifaces == expected_ifaces,

    "five_current_endpoints":
        len(endpoints) == 5
        and all(
            endpoint
            and ":" in endpoint
            for endpoint in endpoints.values()
        ),

    "five_unique_endpoints":
        len(set(endpoints.values())) == 5,

    "five_strict_slots":
        all(strict.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_0420_utc":
        facts.get("cron_0420_found") == "true",

    "read_only":
        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",

    "stderr_clean":
        not unexpected_stderr,
}

warnings = []

if not available_cli:
    warnings.append(
        "no_dedicated_wireguard_runtime_cli_discovered"
    )

if planner.get("changes_count", 0) != 0:
    warnings.append(
        f"planner_changes_count={planner.get('changes_count')}"
    )

all_ok = all(checks.values())

cli_discovery = {
    "candidates": cli_candidates,
    "available": available_cli,
    "dedicated_cli_required_for_checkpoint": False,
    "authoritative_endpoint_source":
        "live router-egress-hmn-plan-top5 current fields",
    "raw_discovery_preserved_in": "vm101.txt",
}

baseline = {
    "captured_at": {
        "epoch": snapshot_epoch,
        "vm101_local":
            facts["vm101_local_time"],
        "vm101_utc":
            facts["vm101_utc_time"],
        "timezone": {
            "etc_tz": facts.get("etc_tz"),
            "uci_timezone":
                facts.get("uci_timezone"),
            "uci_zonename":
                facts.get("uci_zonename"),
        },
    },
    "schedule": {
        "cron": "20 4 * * *",
        "timezone": "UTC",
        "next_run_utc":
            next_run_utc.isoformat(),
        "next_run_amsterdam":
            next_run_amsterdam.isoformat(),
    },
    "runtime_protocol": "AmneziaWG",
    "endpoint_source":
        "live router-egress-hmn-plan-top5 current",
    "cli_discovery": cli_discovery,
    "endpoints": endpoints,
    "strict": strict,
    "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",
    },
    "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"],
    },
    "planner": {
        "decision": planner.get("decision"),
        "changes_count":
            planner.get("changes_count"),
        "plan": plan,
    },
    "runner": {
        "decision": runner.get("decision"),
        "reason": runner.get("reason"),
        "daily_fail_count":
            runner.get("daily_fail_count"),
        "cooldown_remaining":
            runner.get("cooldown_remaining"),
        "last_status":
            runner.get(
                "last_emergency_refresh_status"
            ),
        "last_epoch":
            runner.get(
                "last_emergency_refresh_epoch"
            ),
    },
    "checks": checks,
    "warnings": warnings,
}

comparison_contract = {
    "schema":
        "post-scheduled-refresh-comparison-v4",
    "reference_file": "exact-baseline.json",
    "runtime_protocol": "AmneziaWG",
    "endpoint_source":
        "live planner current fields",
    "compare_after": {
        "utc": next_run_utc.isoformat(),
        "amsterdam":
            next_run_amsterdam.isoformat(),
    },
    "required_comparisons": [
        "planner current endpoints vpn1..vpn5",
        "strict status vpn1..vpn5",
        "discovered runtime CLI and package state",
        "pool sha256, rows and mtime",
        "latest HMN generation",
        "state mode, status and epoch",
        "repair counter",
        "planner decision and changes_count",
        "cron log delta",
        "emergency log delta",
        "commit_failed occurrences",
        "slot_apply_failed_egress1 occurrences",
        "whether scheduled refresh changed live endpoints",
    ],
    "do_not_mutate_before_comparison": True,
}

assessment = {
    "all_ok": all_ok,
    "decision": (
        "PASS_STEP_050M07D5_FINAL_CLI_DISCOVERY_AND_PRE_NIGHT_FREEZE"
        if all_ok
        else
        "STOP_STEP_050M07D5_FINAL_CLI_DISCOVERY_AND_PRE_NIGHT_FREEZE"
    ),
    "checks": checks,
    "warnings": warnings,
    "failed_checks": [
        name
        for name, value in checks.items()
        if not value
    ],
    "cli_discovery": cli_discovery,
    "schedule": baseline["schedule"],
    "exact_endpoints_frozen": all_ok,
    "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 changes. After scheduled refresh, "
        "run read-only comparison."
    ),
}

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

(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 "baseline_and_cli_discovery_built"

stage "05/05" "Публикую финальный checkpoint"

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

ENDPOINTS="$(
  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"{iface}={endpoint}"
        for iface, endpoint
        in sorted(data["endpoints"].items())
    )
)
PY
)"

CLI_SUMMARY="$(
  python3 - "$REPORT_DIR/cli-discovery.json" <<'PY'
import json
import sys

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

available = data["available"]

if available:
    print(
        ",".join(
            f"{name}:{path}"
            for name, path in sorted(available.items())
        )
    )
else:
    print("NO_DEDICATED_CLI_DISCOVERED")
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
)"

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

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

runtime:
  protocol=AmneziaWG
  cli_discovery=${CLI_SUMMARY}
  dedicated_cli_required_for_checkpoint=false
  endpoint_source=live_planner_current
  exact_endpoints_frozen=true
  endpoints=${ENDPOINTS}
  healthy_vpn_slots=5
  five_strict_slots=true
  routes_201_205=true
  services_running_enabled=true
  locks_absent=true

assessment:
  warnings=${WARNINGS}
  warnings_preserved=true
  baseline=exact-baseline.json
  cli_discovery=cli-discovery.json
  comparison_contract=comparison-contract.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

pause_contract:
  changes_before_scheduled_refresh=false
  next_action=READONLY_POST_SCHEDULED_REFRESH_COMPARISON

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": {
        "final_pre_night_checkpoint": True,
        "runtime_protocol": "AmneziaWG",
        "cli_discovery_completed": True,
        "exact_endpoints_frozen": True,
        "schedule_frozen": True,
        "failure_context_preserved": 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 "final_checkpoint_published"
trap - ERR

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

echo "protocol=AmneziaWG" |
  tee -a "$PROGRESS_LOG"

echo "cli_discovery=$CLI_SUMMARY" |
  tee -a "$PROGRESS_LOG"

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

echo "endpoints=$ENDPOINTS" |
  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 "warnings=$WARNINGS" |
  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
