#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
LINUXCNC_ROOT="${LINUXCNC_ROOT:-$ROOT_DIR/../linuxcnc}"
DB_DEMO_DIR="$LINUXCNC_ROOT/configs/sim/axis/db_demo"
DB_PROGRAM="$DB_DEMO_DIR/db_nonran.py"
RUN_DIR="$ROOT_DIR/build/native/tool-db-runtime"
DB_SAVEFILE="$RUN_DIR/db_nonran_probe"
DB_STDOUT="$RUN_DIR/db_program.stdout.log"
DB_STDERR="$RUN_DIR/db_program.stderr.log"

required_commands=(python3 linuxcnc milltask halcmd)
missing_requirements=()

command_path() {
  command -v "$1" 2>/dev/null || true
}

csv_join() {
  local IFS=,
  printf '%s' "$*"
}

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

runtime_requirements=()
for command_name in "${required_commands[@]}"; do
  path="$(command_path "$command_name")"
  if [[ -n "$path" ]]; then
    runtime_requirements+=("$command_name:1")
    print_kv "tool_db_runtime_${command_name}_path" "$path"
  else
    runtime_requirements+=("$command_name:0")
    missing_requirements+=("$command_name")
  fi
done

source_ready=1
for rel in \
  configs/sim/axis/db_demo/db_nonran.ini \
  configs/sim/axis/db_demo/db.py \
  lib/python/tooldb.py \
  lib/python/linuxcnc.so; do
  if [[ ! -e "$LINUXCNC_ROOT/$rel" ]]; then
    source_ready=0
    missing_requirements+=("$rel")
    print_kv "tool_db_missing_source_${rel//[^A-Za-z0-9]/_}" "$LINUXCNC_ROOT/$rel"
  fi
done

if [[ ! -x "$DB_PROGRAM" ]]; then
  source_ready=0
  missing_requirements+=("configs/sim/axis/db_demo/db_nonran.py:executable")
fi

runtime_ready=0
if [[ "${#missing_requirements[@]}" -eq 0 ]]; then
  runtime_ready=1
fi

print_kv tool_db_runtime_requirements "$(csv_join "${runtime_requirements[@]}")"
if [[ "${#missing_requirements[@]}" -eq 0 ]]; then
  print_kv tool_db_missing_requirements "-"
else
  print_kv tool_db_missing_requirements "$(csv_join "${missing_requirements[@]}")"
fi
print_kv tool_db_source_proof_ready "$source_ready"
print_kv tool_db_runtime_ready "$runtime_ready"
print_kv tool_db_execution_enabled 0
print_kv tool_db_promotion_allowed 0

if [[ "$source_ready" != "1" ]]; then
  print_kv tool_db_runtime_probe_status "blocked_missing_source"
  print_kv tool_db_runtime_probe_note "missing_source_requirements_keep_tool_db_process_blocked"
  exit 0
fi

if [[ "$runtime_ready" != "1" ]]; then
  print_kv tool_db_runtime_probe_status "skipped_missing_host_runtime"
  print_kv tool_db_runtime_probe_note "missing_host_runtime_requirements_keep_tool_db_process_blocked"
  exit 0
fi

if [[ "${ENABLE_TOOL_DB_RUNTIME_PROBE:-0}" != "1" ]]; then
  print_kv tool_db_runtime_probe_status "ready_disabled_by_default"
  print_kv tool_db_runtime_probe_note "set_ENABLE_TOOL_DB_RUNTIME_PROBE_1_to_run_db_protocol_probe"
  exit 0
fi

mkdir -p "$RUN_DIR"
rm -f "$DB_SAVEFILE" "$DB_STDOUT" "$DB_STDERR"

print_kv tool_db_runtime_probe_program "$DB_PROGRAM"
print_kv tool_db_runtime_probe_db_savefile "$DB_SAVEFILE"
print_kv tool_db_runtime_probe_stdout "$DB_STDOUT"
print_kv tool_db_runtime_probe_stderr "$DB_STDERR"

if python3 - "$DB_PROGRAM" "$DB_DEMO_DIR" "$LINUXCNC_ROOT/lib/python" "$DB_SAVEFILE" "$DB_STDOUT" "$DB_STDERR" <<'PY'
import os
import select
import subprocess
import sys
import time

db_program, db_demo_dir, python_path, db_savefile, stdout_log, stderr_log = sys.argv[1:]

env = os.environ.copy()
env["PYTHONPATH"] = python_path + (os.pathsep + env["PYTHONPATH"] if env.get("PYTHONPATH") else "")

stdout_file = open(stdout_log, "w", encoding="utf-8")
stderr_file = open(stderr_log, "w", encoding="utf-8")
proc = subprocess.Popen(
    [db_program, "--period_minutes=999", db_savefile],
    cwd=db_demo_dir,
    env=env,
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    stderr=stderr_file,
    text=True,
    bufsize=1,
)

def emit(name, value):
    print(f"{name}={value}")

def read_line(timeout=5.0):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        ready, _, _ = select.select([proc.stdout], [], [], 0.1)
        if ready:
            line = proc.stdout.readline()
            if line == "":
                raise RuntimeError("db_program_stdout_closed")
            line = line.rstrip("\n")
            stdout_file.write(line + "\n")
            stdout_file.flush()
            return line
        if proc.poll() is not None:
            raise RuntimeError(f"db_program_exited_{proc.returncode}")
    raise RuntimeError("timeout_waiting_for_db_program_reply")

def send(command):
    proc.stdin.write(command + "\n")
    proc.stdin.flush()

def read_until_fini(command, timeout=5.0):
    send(command)
    lines = []
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        line = read_line(max(0.1, deadline - time.monotonic()))
        lines.append(line)
        if line.startswith("FINI"):
            return lines
    raise RuntimeError(f"timeout_waiting_for_fini_{command.split()[0]}")

def require(condition, message):
    if not condition:
        raise RuntimeError(message)

ok = False
try:
    version = read_line()
    emit("tool_db_protocol_version", version)
    require(version == "v2.1", f"version_drift:{version}")

    get_all = read_until_fini("g")
    tool_lines = [line for line in get_all if line.startswith("T")]
    emit("tool_db_get_all_count", len(tool_lines))
    require(len(tool_lines) == 10, "get_all_tool_count_drift")
    for toolno in range(10, 20):
        expected = f"T{toolno} "
        require(any(line.startswith(expected) for line in tool_lines), f"missing_tool_{toolno}")
        require(any(line.startswith(expected) and f"P{toolno + 100}" in line for line in tool_lines), f"pocket_drift_{toolno}")
    emit("tool_db_get_all_fini", int(get_all[-1].startswith("FINI")))

    put_reply = read_until_fini("p t11 p111 d0.33 z0.11")
    emit("tool_db_put_tool_update_fini", int(put_reply[-1].startswith("FINI")))

    get_after_put = read_until_fini("g")
    t11_rows = [line for line in get_after_put if line.startswith("T11 ")]
    require(t11_rows and "D0.33" in t11_rows[0] and "Z0.11" in t11_rows[0], "put_update_state_drift")
    emit("tool_db_put_tool_update_state_ok", 1)

    load_reply = read_until_fini("l t14 p0")
    emit("tool_db_load_spindle_fini", int(load_reply[-1].startswith("FINI")))
    t14_loaded = read_until_fini("t 14")
    require(any(line.startswith("T14 ") and "P0" in line for line in t14_loaded), "load_spindle_state_drift")
    emit("tool_db_load_spindle_state_ok", 1)

    unload_reply = read_until_fini("u t0 p0")
    emit("tool_db_unload_spindle_fini", int(unload_reply[-1].startswith("FINI")))
    t14_unloaded = read_until_fini("t 14")
    require(any(line.startswith("T14 ") and "P114" in line for line in t14_unloaded), "unload_spindle_state_drift")
    emit("tool_db_unload_spindle_state_ok", 1)

    require(os.path.exists(db_savefile), "db_savefile_missing")
    with open(db_savefile, "r", encoding="utf-8") as handle:
        saved = handle.read()
    require("T11 P111 D0.33 Z0.11" in saved, "db_savefile_put_update_missing")
    require("T14 P114" in saved, "db_savefile_unload_state_missing")
    emit("tool_db_persistence_state_ok", 1)
    ok = True
finally:
    try:
        if proc.stdin:
            proc.stdin.close()
    except Exception:
        pass
    if proc.poll() is None:
        proc.terminate()
        try:
            proc.wait(timeout=2)
        except subprocess.TimeoutExpired:
            proc.kill()
            proc.wait(timeout=2)
    stderr_file.close()
    stdout_file.close()

emit("tool_db_runtime_protocol_probe_ok", int(ok))
sys.exit(0 if ok else 1)
PY
then
  print_kv tool_db_runtime_probe_status "runtime_protocol_probe_passed"
  print_kv tool_db_runtime_probe_note "linuxcnc_owned_DB_PROGRAM_protocol_probe_passed_without_promotion"
  exit 0
fi

print_kv tool_db_runtime_probe_status "runtime_protocol_probe_failed"
print_kv tool_db_runtime_probe_note "linuxcnc_owned_DB_PROGRAM_protocol_probe_failed"
exit 1
