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

APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME_DIR="${TRADECAT_AUTO_PAPER_RUNTIME_DIR:-$APP_DIR/.runtime/auto-paper}"
SYSTEMD_USER_DIR="${TRADECAT_AUTO_PAPER_SYSTEMD_USER_DIR:-${HOME:-}/.config/systemd/user}"
SYSTEMCTL_BIN="${TRADECAT_AUTO_PAPER_SYSTEMCTL_BIN:-systemctl}"
SYSTEMD_SERVICE_UNIT="tradecat-auto-paper.service"
# Legacy unit name. New installs use the long-running service as the single owner.
SYSTEMD_TIMER_UNIT="tradecat-auto-paper.timer"
INTERVAL_SECONDS="${TRADECAT_AUTO_PAPER_INTERVAL_SECONDS:-60}"
MAINTENANCE_INTERVAL_SECONDS="${TRADECAT_AUTO_PAPER_MAINTENANCE_INTERVAL_SECONDS:-300}"
CYCLE_TIMEOUT_SECONDS="${TRADECAT_AUTO_PAPER_CYCLE_TIMEOUT_SECONDS:-6000}"
PAPER_MARGIN_BUDGET_USDT="${TRADECAT_AUTO_PAPER_MARGIN_BUDGET_USDT:-}"
AGENT_MARGIN_USDT="${TRADECAT_AUTO_PAPER_AGENT_MARGIN_USDT:-}"
EFFECTIVE_NOTIONAL_USDT="${TRADECAT_AUTO_PAPER_EFFECTIVE_NOTIONAL_USDT:-}"
PAPER_LEVERAGE="${TRADECAT_AUTO_PAPER_LEVERAGE:-}"
AGENT_TRADE_THESIS_PATH="${TRADECAT_AUTO_PAPER_AGENT_TRADE_THESIS_PATH:-}"
PAPER_AUTONOMY_PROFILE_PATH="${TRADECAT_AUTO_PAPER_AUTONOMY_PROFILE_PATH:-}"
PAPER_AUTONOMY_ENABLED="${TRADECAT_AUTO_PAPER_AUTONOMY_ENABLED:-0}"
PAPER_AUTONOMY_PROFILE_DEFAULTED=0
if [[ -z "$AGENT_TRADE_THESIS_PATH" && -z "$PAPER_AUTONOMY_PROFILE_PATH" && "$PAPER_AUTONOMY_ENABLED" != "0" && "$PAPER_AUTONOMY_ENABLED" != "false" ]]; then
  PAPER_AUTONOMY_PROFILE_PATH="$RUNTIME_DIR/paper_autonomy_profile.json"
  PAPER_AUTONOMY_PROFILE_DEFAULTED=1
fi
PAPER_AUTONOMY_MARGIN_USDT="${TRADECAT_AUTO_PAPER_AUTONOMY_MARGIN_USDT:-10}"
PAPER_AUTONOMY_LEVERAGE="${TRADECAT_AUTO_PAPER_AUTONOMY_LEVERAGE:-1}"
PAPER_AUTONOMY_STOP_LOSS_BPS="${TRADECAT_AUTO_PAPER_AUTONOMY_STOP_LOSS_BPS:-150}"
PAPER_AUTONOMY_TAKE_PROFIT_BPS="${TRADECAT_AUTO_PAPER_AUTONOMY_TAKE_PROFIT_BPS:-300}"
PAPER_AUTONOMY_MAX_HOLDING_MINUTES="${TRADECAT_AUTO_PAPER_AUTONOMY_MAX_HOLDING_MINUTES:-90}"
PAPER_AUTONOMY_DIRECTION_POLICY="${TRADECAT_AUTO_PAPER_AUTONOMY_DIRECTION_POLICY:-sheet_signal_or_taker_flow}"
PAPER_AUTONOMY_MIN_SIGNAL_SCORE="${TRADECAT_AUTO_PAPER_AUTONOMY_MIN_SIGNAL_SCORE:-0}"
PAPER_AUTONOMY_ALLOW_MULTIPLE="${TRADECAT_AUTO_PAPER_AUTONOMY_ALLOW_MULTIPLE:-1}"
PAPER_AUTONOMY_MAX_CONCURRENT_PER_SYMBOL="${TRADECAT_AUTO_PAPER_AUTONOMY_MAX_CONCURRENT_PER_SYMBOL:-}"
INITIAL_BALANCE_USDT="${TRADECAT_AUTO_PAPER_INITIAL_BALANCE_USDT:-1000}"
PAPER_FEE_BPS="${TRADECAT_AUTO_PAPER_FEE_BPS:-4}"
PAPER_SLIPPAGE_BPS="${TRADECAT_AUTO_PAPER_SLIPPAGE_BPS:-0}"
PAPER_MAX_HOLDING_MINUTES="${TRADECAT_AUTO_PAPER_MAX_HOLDING_MINUTES:-0}"
MAX_EVENT_AGE_SECONDS="${TRADECAT_AUTO_PAPER_MAX_EVENT_AGE_SECONDS:-}"
EVENT_LIMIT="${TRADECAT_AUTO_PAPER_EVENT_LIMIT:-0}"
ANOMALY_LIMIT="${TRADECAT_AUTO_PAPER_ANOMALY_LIMIT:-0}"
SYMBOL="${TRADECAT_AUTO_PAPER_SYMBOL:-auto}"
BASE_URL="${TRADECAT_AUTO_PAPER_BASE_URL:-https://fapi.binance.com}"
MAX_HEARTBEAT_AGE_SECONDS="${TRADECAT_AUTO_PAPER_MAX_HEARTBEAT_AGE_SECONDS:-180}"
PORTFOLIO_RISK_POLICY_PATH="${TRADECAT_AUTO_PAPER_PORTFOLIO_RISK_POLICY_PATH:-}"
PAPER_KILL_SWITCH_PATH="${TRADECAT_AUTO_PAPER_KILL_SWITCH_PATH:-}"
STRATEGY_REVIEW_ENABLED="${TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_ENABLED:-1}"
STRATEGY_STATE_PATH="${TRADECAT_AUTO_PAPER_STRATEGY_STATE_PATH:-$RUNTIME_DIR/strategy_state.json}"
STRATEGY_REVIEW_REPORT_PATH="${TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_REPORT_PATH:-$RUNTIME_DIR/strategy-review-latest.json}"
STRATEGY_REVIEW_MIN_CLOSED_POSITIONS="${TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_MIN_CLOSED_POSITIONS:-50}"
STRATEGY_REVIEW_MAX_OPEN_POSITIONS="${TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_MAX_OPEN_POSITIONS:-50}"
STRATEGY_REVIEW_MAX_POSITIONS_PER_SYMBOL="${TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_MAX_POSITIONS_PER_SYMBOL:-3}"
MIN_FREE_BYTES="${TRADECAT_AUTO_PAPER_MIN_FREE_BYTES:-104857600}"
MIN_NOFILE="${TRADECAT_AUTO_PAPER_MIN_NOFILE:-1024}"
MIN_NPROC="${TRADECAT_AUTO_PAPER_MIN_NPROC:-128}"
LIMIT_NOFILE="${TRADECAT_AUTO_PAPER_LIMIT_NOFILE:-4096}"
TASKS_MAX="${TRADECAT_AUTO_PAPER_TASKS_MAX:-64}"
RESTART_SEC="${TRADECAT_AUTO_PAPER_RESTART_SEC:-30s}"
START_LIMIT_BURST="${TRADECAT_AUTO_PAPER_START_LIMIT_BURST:-5}"
START_LIMIT_INTERVAL_SECONDS="${TRADECAT_AUTO_PAPER_START_LIMIT_INTERVAL_SECONDS:-600}"
TIMEOUT_START_SECONDS="${TRADECAT_AUTO_PAPER_TIMEOUT_START_SECONDS:-120}"
STATE_PATH="${TRADECAT_AUTO_PAPER_STATE_PATH:-$RUNTIME_DIR/service_state.json}"
LEDGER_PATH="${TRADECAT_AUTO_PAPER_LEDGER_PATH:-$RUNTIME_DIR/paper_ledger.json}"
ARCHIVE_PATH="${TRADECAT_AUTO_PAPER_ARCHIVE_PATH:-$RUNTIME_DIR/cycles.jsonl}"
JOURNAL_PATH="${TRADECAT_AUTO_PAPER_JOURNAL_PATH:-$RUNTIME_DIR/paper_audit.sqlite3}"
PID_FILE="$RUNTIME_DIR/paper-run-loop.pid"
LOG_FILE="${TRADECAT_AUTO_PAPER_LOG_FILE:-$RUNTIME_DIR/paper-run-loop.log}"
ACTION="status"
JSON=0

for arg in "$@"; do
  case "$arg" in
    --json) JSON=1 ;;
    start|stop|restart|status|ops-check|heal|systemd-install|systemd-uninstall|health|daily|alert|_run|_cycle) ACTION="$arg" ;;
    *) echo "usage: $0 [--json] start|stop|restart|status|ops-check|heal|health|daily|alert|systemd-install|systemd-uninstall" >&2; exit 2 ;;
  esac
done

python_bin() {
  if [[ -n "${PYTHON_BIN:-}" ]]; then
    printf '%s\n' "$PYTHON_BIN"
    return 0
  fi
  if [[ -x "$APP_DIR/.venv/bin/python" ]]; then
    printf '%s\n' "$APP_DIR/.venv/bin/python"
    return 0
  fi
  printf '%s\n' "python3"
}

read_pid() {
  if [[ -f "$PID_FILE" ]]; then
    cat "$PID_FILE"
  fi
}

is_running() {
  local pid
  pid="$(read_pid)"
  [[ -n "$pid" ]] || return 1
  kill -0 "$pid" >/dev/null 2>&1 || return 1
  if [[ -r "/proc/$pid/cmdline" ]]; then
    tr '\0' ' ' <"/proc/$pid/cmdline" | grep -F "start-auto-paper.sh _run" >/dev/null 2>&1
    return $?
  fi
  return 0
}

write_own_pid_file() {
  mkdir -p "$RUNTIME_DIR"
  printf '%s\n' "$$" >"$PID_FILE"
}

clear_own_pid_file() {
  local pid
  pid="$(read_pid)"
  if [[ "$pid" == "$$" ]]; then
    rm -f "$PID_FILE"
  fi
}

stop_running_loop_quietly() {
  if ! is_running; then
    rm -f "$PID_FILE"
    return 0
  fi
  local pid
  pid="$(read_pid)"
  kill "$pid" >/dev/null 2>&1 || true
  for _ in $(seq 1 30); do
    if ! kill -0 "$pid" >/dev/null 2>&1; then
      rm -f "$PID_FILE"
      return 0
    fi
    sleep 0.2
  done
  kill -9 "$pid" >/dev/null 2>&1 || true
  rm -f "$PID_FILE"
}

proc_env_value() {
  local pid="$1"
  local key="$2"
  local env_path="/proc/$pid/environ"
  local entry
  local prefix="$key="
  [[ -r "$env_path" ]] || return 1
  while IFS= read -r -d '' entry; do
    if [[ "$entry" == "$prefix"* ]]; then
      printf '%s\n' "${entry#"$prefix"}"
      return 0
    fi
  done <"$env_path"
  return 1
}

load_running_env_config() {
  local pid="$1"
  local value
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_INTERVAL_SECONDS")"; then INTERVAL_SECONDS="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_MAINTENANCE_INTERVAL_SECONDS")"; then MAINTENANCE_INTERVAL_SECONDS="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_CYCLE_TIMEOUT_SECONDS")"; then CYCLE_TIMEOUT_SECONDS="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_MARGIN_BUDGET_USDT")"; then PAPER_MARGIN_BUDGET_USDT="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_AGENT_MARGIN_USDT")"; then AGENT_MARGIN_USDT="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_EFFECTIVE_NOTIONAL_USDT")"; then EFFECTIVE_NOTIONAL_USDT="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_LEVERAGE")"; then PAPER_LEVERAGE="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_AGENT_TRADE_THESIS_PATH")"; then AGENT_TRADE_THESIS_PATH="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_AUTONOMY_PROFILE_PATH")"; then PAPER_AUTONOMY_PROFILE_PATH="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_INITIAL_BALANCE_USDT")"; then INITIAL_BALANCE_USDT="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_FEE_BPS")"; then PAPER_FEE_BPS="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_SLIPPAGE_BPS")"; then PAPER_SLIPPAGE_BPS="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_MAX_HOLDING_MINUTES")"; then PAPER_MAX_HOLDING_MINUTES="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_MAX_EVENT_AGE_SECONDS")"; then MAX_EVENT_AGE_SECONDS="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_EVENT_LIMIT")"; then EVENT_LIMIT="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_ANOMALY_LIMIT")"; then ANOMALY_LIMIT="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_SYMBOL")"; then SYMBOL="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_BASE_URL")"; then BASE_URL="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_PORTFOLIO_RISK_POLICY_PATH")"; then PORTFOLIO_RISK_POLICY_PATH="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_KILL_SWITCH_PATH")"; then PAPER_KILL_SWITCH_PATH="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_ENABLED")"; then STRATEGY_REVIEW_ENABLED="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_STRATEGY_STATE_PATH")"; then STRATEGY_STATE_PATH="$value"; fi
  if value="$(proc_env_value "$pid" "TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_REPORT_PATH")"; then STRATEGY_REVIEW_REPORT_PATH="$value"; fi
}

emit_json() {
  local action="$1"
  local state="$2"
  local running="$3"
  local ok="$4"
  local event="$5"
  local pid="${6:-}"
  local error_code="${7:-}"
  local error_message="${8:-}"
  local py
  py="$(python_bin)"
  AUTO_JSON_ACTION="$action" \
  AUTO_JSON_STATE="$state" \
  AUTO_JSON_RUNNING="$running" \
  AUTO_JSON_OK="$ok" \
  AUTO_JSON_EVENT="$event" \
  AUTO_JSON_PID="$pid" \
  AUTO_JSON_ERROR_CODE="$error_code" \
  AUTO_JSON_ERROR_MESSAGE="$error_message" \
  AUTO_JSON_RUNTIME_DIR="$RUNTIME_DIR" \
  AUTO_JSON_STATE_PATH="$STATE_PATH" \
  AUTO_JSON_LEDGER_PATH="$LEDGER_PATH" \
  AUTO_JSON_ARCHIVE_PATH="$ARCHIVE_PATH" \
  AUTO_JSON_JOURNAL_PATH="$JOURNAL_PATH" \
  AUTO_JSON_LOG_FILE="$LOG_FILE" \
  AUTO_JSON_SYSTEMD_USER_DIR="$SYSTEMD_USER_DIR" \
  AUTO_JSON_SYSTEMD_SERVICE_UNIT="$SYSTEMD_SERVICE_UNIT" \
  AUTO_JSON_SYSTEMD_TIMER_UNIT="$SYSTEMD_TIMER_UNIT" \
  AUTO_JSON_INTERVAL_SECONDS="$INTERVAL_SECONDS" \
  AUTO_JSON_MAINTENANCE_INTERVAL_SECONDS="$MAINTENANCE_INTERVAL_SECONDS" \
  AUTO_JSON_CYCLE_TIMEOUT_SECONDS="$CYCLE_TIMEOUT_SECONDS" \
  AUTO_JSON_PAPER_MARGIN_BUDGET_USDT="$PAPER_MARGIN_BUDGET_USDT" \
  AUTO_JSON_AGENT_MARGIN_USDT="$AGENT_MARGIN_USDT" \
  AUTO_JSON_EFFECTIVE_NOTIONAL_USDT="$EFFECTIVE_NOTIONAL_USDT" \
  AUTO_JSON_PAPER_LEVERAGE="$PAPER_LEVERAGE" \
  AUTO_JSON_AGENT_TRADE_THESIS_PATH="$AGENT_TRADE_THESIS_PATH" \
  AUTO_JSON_PAPER_AUTONOMY_PROFILE_PATH="$PAPER_AUTONOMY_PROFILE_PATH" \
  AUTO_JSON_PAPER_AUTONOMY_ENABLED="$PAPER_AUTONOMY_ENABLED" \
  AUTO_JSON_PAPER_AUTONOMY_PROFILE_DEFAULTED="$PAPER_AUTONOMY_PROFILE_DEFAULTED" \
  AUTO_JSON_PAPER_AUTONOMY_MARGIN_USDT="$PAPER_AUTONOMY_MARGIN_USDT" \
  AUTO_JSON_PAPER_AUTONOMY_LEVERAGE="$PAPER_AUTONOMY_LEVERAGE" \
  AUTO_JSON_INITIAL_BALANCE_USDT="$INITIAL_BALANCE_USDT" \
  AUTO_JSON_PAPER_FEE_BPS="$PAPER_FEE_BPS" \
  AUTO_JSON_PAPER_SLIPPAGE_BPS="$PAPER_SLIPPAGE_BPS" \
  AUTO_JSON_PAPER_MAX_HOLDING_MINUTES="$PAPER_MAX_HOLDING_MINUTES" \
  AUTO_JSON_MAX_EVENT_AGE_SECONDS="$MAX_EVENT_AGE_SECONDS" \
  AUTO_JSON_EVENT_LIMIT="$EVENT_LIMIT" \
  AUTO_JSON_ANOMALY_LIMIT="$ANOMALY_LIMIT" \
  AUTO_JSON_SYMBOL="$SYMBOL" \
  AUTO_JSON_BASE_URL="$BASE_URL" \
  AUTO_JSON_PORTFOLIO_RISK_POLICY_PATH="$PORTFOLIO_RISK_POLICY_PATH" \
  AUTO_JSON_PAPER_KILL_SWITCH_PATH="$PAPER_KILL_SWITCH_PATH" \
  AUTO_JSON_STRATEGY_REVIEW_ENABLED="$STRATEGY_REVIEW_ENABLED" \
  AUTO_JSON_STRATEGY_STATE_PATH="$STRATEGY_STATE_PATH" \
  AUTO_JSON_STRATEGY_REVIEW_REPORT_PATH="$STRATEGY_REVIEW_REPORT_PATH" \
  AUTO_JSON_PYTHON="$py" \
  "$py" - <<'PY'
import json
import os


def truthy(value: str) -> bool:
    return value.lower() in {"1", "true", "yes", "on"}


def optional_int(value: str):
    if not value:
        return None
    try:
        return int(value)
    except ValueError:
        return None


def optional_float(value: str):
    if value == "":
        return None
    try:
        return float(value)
    except ValueError:
        return value

action = os.environ["AUTO_JSON_ACTION"]
state = os.environ["AUTO_JSON_STATE"]
event = os.environ["AUTO_JSON_EVENT"]
running = truthy(os.environ["AUTO_JSON_RUNNING"])
pid = optional_int(os.environ["AUTO_JSON_PID"])
ok = truthy(os.environ["AUTO_JSON_OK"])
if not running:
    process_health = "not_running"
elif event in {"running", "already_running", "service_enabled"} and pid is not None:
    process_health = "running_pid_verified"
else:
    process_health = "spawned_unverified"
paper_margin_budget_usdt = optional_float(os.environ["AUTO_JSON_PAPER_MARGIN_BUDGET_USDT"])
agent_margin_usdt = optional_float(os.environ["AUTO_JSON_AGENT_MARGIN_USDT"])
explicit_effective_notional_usdt = optional_float(os.environ["AUTO_JSON_EFFECTIVE_NOTIONAL_USDT"])
paper_leverage = optional_float(os.environ["AUTO_JSON_PAPER_LEVERAGE"])
agent_trade_thesis_path = os.environ["AUTO_JSON_AGENT_TRADE_THESIS_PATH"]
paper_autonomy_profile_path = os.environ["AUTO_JSON_PAPER_AUTONOMY_PROFILE_PATH"]
paper_autonomy_enabled = truthy(os.environ["AUTO_JSON_PAPER_AUTONOMY_ENABLED"])
paper_autonomy_profile_defaulted = truthy(os.environ["AUTO_JSON_PAPER_AUTONOMY_PROFILE_DEFAULTED"])
paper_autonomy_margin_usdt = optional_float(os.environ["AUTO_JSON_PAPER_AUTONOMY_MARGIN_USDT"])
paper_autonomy_leverage = optional_float(os.environ["AUTO_JSON_PAPER_AUTONOMY_LEVERAGE"])
effective_notional_usdt = (
    agent_margin_usdt * paper_leverage
    if isinstance(agent_margin_usdt, (int, float)) and isinstance(paper_leverage, (int, float))
    else explicit_effective_notional_usdt if isinstance(explicit_effective_notional_usdt, (int, float)) and isinstance(paper_leverage, (int, float))
    else paper_autonomy_margin_usdt * paper_autonomy_leverage
    if paper_autonomy_profile_defaulted and isinstance(paper_autonomy_margin_usdt, (int, float)) and isinstance(paper_autonomy_leverage, (int, float))
    else None
)
resolved_margin_usdt = agent_margin_usdt
resolved_leverage = paper_leverage
if paper_autonomy_profile_defaulted and resolved_margin_usdt is None:
    resolved_margin_usdt = paper_autonomy_margin_usdt
if paper_autonomy_profile_defaulted and resolved_leverage is None:
    resolved_leverage = paper_autonomy_leverage
sizing_source = (
    "service_environment"
    if isinstance(agent_margin_usdt, (int, float)) or isinstance(explicit_effective_notional_usdt, (int, float))
    else "agent_trade_thesis"
    if agent_trade_thesis_path
    else "paper_autonomy_profile"
    if paper_autonomy_profile_path
    else "agent_required_missing"
)
payload = {
    "schema": "tradecat_auto.paper_service_status.v1",
    "schema_version": "1.0.0",
    "ok": ok,
    "mode": "paper",
    "real_orders": False,
    "signed_requests": False,
    "reads_api_keys": False,
    "command": "auto-paper-service",
    "action": action,
    "state": state,
    "event": event,
    "running": running,
    "pid": pid,
    "runtime_dir": os.environ["AUTO_JSON_RUNTIME_DIR"],
    "state_path": os.environ["AUTO_JSON_STATE_PATH"],
    "ledger_path": os.environ["AUTO_JSON_LEDGER_PATH"],
    "archive_path": os.environ["AUTO_JSON_ARCHIVE_PATH"],
    "journal_path": os.environ["AUTO_JSON_JOURNAL_PATH"],
    "log_file": os.environ["AUTO_JSON_LOG_FILE"],
    "systemd_user_dir": os.environ["AUTO_JSON_SYSTEMD_USER_DIR"],
    "systemd_service_unit": os.environ["AUTO_JSON_SYSTEMD_SERVICE_UNIT"],
    "systemd_timer_unit": os.environ["AUTO_JSON_SYSTEMD_TIMER_UNIT"],
    "systemd_lifecycle_owner": "service",
    "interval_seconds": optional_float(os.environ["AUTO_JSON_INTERVAL_SECONDS"]),
    "maintenance_interval_seconds": optional_float(os.environ["AUTO_JSON_MAINTENANCE_INTERVAL_SECONDS"]),
    "cycle_timeout_seconds": optional_float(os.environ["AUTO_JSON_CYCLE_TIMEOUT_SECONDS"]),
    "paper_margin_budget_usdt": paper_margin_budget_usdt,
    "agent_margin_usdt": agent_margin_usdt,
    "notional_usdt": explicit_effective_notional_usdt,
    "notional_semantics": "deprecated explicit effective notional; no default paper order amount or budget cap",
    "paper_leverage": paper_leverage,
    "agent_trade_thesis_path": agent_trade_thesis_path,
    "agent_trade_thesis_configured": bool(agent_trade_thesis_path),
    "paper_autonomy_profile_path": paper_autonomy_profile_path,
    "paper_autonomy_profile_configured": bool(paper_autonomy_profile_path),
    "paper_autonomy_enabled": paper_autonomy_enabled,
    "paper_autonomy_profile_defaulted": paper_autonomy_profile_defaulted,
    "effective_notional_usdt": effective_notional_usdt,
    "agent_sizing_required": effective_notional_usdt is None and not agent_trade_thesis_path and not paper_autonomy_profile_path,
    "paper_sizing": {
        "schema": "tradecat_auto.paper_sizing_decision.v1",
        "schema_version": "1.0.0",
        "source": sizing_source,
        "margin_budget_usdt": paper_margin_budget_usdt,
        "requested_margin_usdt": resolved_margin_usdt,
        "paper_leverage": resolved_leverage,
        "effective_notional_usdt": effective_notional_usdt,
        "notional_semantics": "effective_notional_usdt; no default paper order amount or budget cap",
    },
    "initial_balance_usdt": optional_float(os.environ["AUTO_JSON_INITIAL_BALANCE_USDT"]),
    "paper_fee_bps": optional_float(os.environ["AUTO_JSON_PAPER_FEE_BPS"]),
    "paper_fee_model": "binance_usdm_public_docs_vip0_taker_fallback",
    "paper_slippage_bps": optional_float(os.environ["AUTO_JSON_PAPER_SLIPPAGE_BPS"]),
    "paper_max_holding_minutes": optional_float(os.environ["AUTO_JSON_PAPER_MAX_HOLDING_MINUTES"]),
    "paper_max_holding_minutes_semantics": "legacy status/config field only; time stops require Agent strategy_intent/agent_trade_thesis max_holding_minutes on the paper position",
    "max_event_age_seconds": optional_float(os.environ["AUTO_JSON_MAX_EVENT_AGE_SECONDS"]),
    "event_limit": optional_int(os.environ["AUTO_JSON_EVENT_LIMIT"]),
    "anomaly_limit": optional_int(os.environ["AUTO_JSON_ANOMALY_LIMIT"]),
    "symbol": os.environ["AUTO_JSON_SYMBOL"],
    "base_url": os.environ["AUTO_JSON_BASE_URL"],
    "portfolio_risk_policy_path": os.environ["AUTO_JSON_PORTFOLIO_RISK_POLICY_PATH"],
    "paper_kill_switch_path": os.environ["AUTO_JSON_PAPER_KILL_SWITCH_PATH"],
    "strategy_review_enabled": truthy(os.environ["AUTO_JSON_STRATEGY_REVIEW_ENABLED"]),
    "strategy_state_path": os.environ["AUTO_JSON_STRATEGY_STATE_PATH"],
    "strategy_state_configured": bool(os.environ["AUTO_JSON_STRATEGY_STATE_PATH"]),
    "strategy_review_report_path": os.environ["AUTO_JSON_STRATEGY_REVIEW_REPORT_PATH"],
    "python": os.environ["AUTO_JSON_PYTHON"],
    "health": process_health,
    "process_health": process_health,
    "health_report_command": "bash scripts/start-auto-paper.sh health --json",
    "safety": {
        "public_readonly_market_data": True,
        "public_readonly": True,
        "paper_or_watch_only": True,
        "real_orders": False,
        "signed_requests": False,
        "reads_api_keys": False,
        "binance_account_state": False,
    },
    "limitations": [
        "public Binance market data only",
        "paper/watch mode only",
        "no Binance API keys are read",
        "no signed account or order endpoints are called",
        "no real order is placed",
    ],
}
if not ok:
    payload["error"] = {
        "code": os.environ["AUTO_JSON_ERROR_CODE"] or "paper_service_not_running",
        "kind": "runtime",
        "message": os.environ["AUTO_JSON_ERROR_MESSAGE"] or "auto paper run-loop is not running",
        "hint": "Run scripts/start-auto-paper.sh start --json, then inspect paper-report and .runtime/auto-paper/cycles.jsonl.",
        "retryable": False,
    }
print(json.dumps(payload, ensure_ascii=False))
PY
}

json_flag_args() {
  if [[ "$JSON" -eq 1 ]]; then
    printf '%s\n' "--json"
  fi
}

emit_text_or_json() {
  local action="$1"
  local state="$2"
  local running="$3"
  local ok="$4"
  local event="$5"
  local text="$6"
  local pid="${7:-}"
  local error_code="${8:-}"
  local error_message="${9:-}"
  if [[ "$JSON" -eq 1 ]]; then
    emit_json "$action" "$state" "$running" "$ok" "$event" "$pid" "$error_code" "$error_message"
  else
    echo "$text"
  fi
}

ensure_paper_autonomy_profile() {
  if [[ "$PAPER_AUTONOMY_PROFILE_DEFAULTED" != "1" || -z "$PAPER_AUTONOMY_PROFILE_PATH" ]]; then
    return 0
  fi
  mkdir -p "$(dirname "$PAPER_AUTONOMY_PROFILE_PATH")"
  local py
  py="$(python_bin)"
  AUTO_PROFILE_PATH="$PAPER_AUTONOMY_PROFILE_PATH" \
  AUTO_PROFILE_MARGIN_USDT="$PAPER_AUTONOMY_MARGIN_USDT" \
  AUTO_PROFILE_LEVERAGE="$PAPER_AUTONOMY_LEVERAGE" \
  AUTO_PROFILE_STOP_LOSS_BPS="$PAPER_AUTONOMY_STOP_LOSS_BPS" \
  AUTO_PROFILE_TAKE_PROFIT_BPS="$PAPER_AUTONOMY_TAKE_PROFIT_BPS" \
  AUTO_PROFILE_MAX_HOLDING_MINUTES="$PAPER_AUTONOMY_MAX_HOLDING_MINUTES" \
  AUTO_PROFILE_DIRECTION_POLICY="$PAPER_AUTONOMY_DIRECTION_POLICY" \
  AUTO_PROFILE_MIN_SIGNAL_SCORE="$PAPER_AUTONOMY_MIN_SIGNAL_SCORE" \
  AUTO_PROFILE_ALLOW_MULTIPLE="$PAPER_AUTONOMY_ALLOW_MULTIPLE" \
  AUTO_PROFILE_MAX_CONCURRENT_PER_SYMBOL="$PAPER_AUTONOMY_MAX_CONCURRENT_PER_SYMBOL" \
  "$py" - <<'PY'
import json
import os
from pathlib import Path


def as_float(name: str, default: float) -> float:
    try:
        value = float(os.environ.get(name, ""))
    except ValueError:
        value = default
    return value if value > 0 else default


def truthy(value: str) -> bool:
    return value.lower() in {"1", "true", "yes", "on"}


path = Path(os.environ["AUTO_PROFILE_PATH"])
profile = {
    "schema": "tradecat_auto.paper_autonomy_profile.v1",
    "schema_version": "1.0.0",
    "ok": True,
    "enabled": True,
    "mode": "paper",
    "holding_horizon": "intraday",
    "paper_intent": {
        "allow_tradecat_paper_gate_to_decide": True,
        "requested_margin_usdt": as_float("AUTO_PROFILE_MARGIN_USDT", 10.0),
        "paper_leverage": as_float("AUTO_PROFILE_LEVERAGE", 1.0),
        "allow_agent_direction_override": True,
        "allow_signal_reject_override": True,
        "direction_policy": os.environ.get("AUTO_PROFILE_DIRECTION_POLICY") or "sheet_signal_or_taker_flow",
        "min_signal_score": max(0.0, as_float("AUTO_PROFILE_MIN_SIGNAL_SCORE", 0.0)),
        "allow_multiple_open_positions_per_symbol": truthy(os.environ.get("AUTO_PROFILE_ALLOW_MULTIPLE", "1")),
        "real_order": False,
    },
    "exit_plan": {
        "stop_loss_bps": as_float("AUTO_PROFILE_STOP_LOSS_BPS", 150.0),
        "take_profit_bps": as_float("AUTO_PROFILE_TAKE_PROFIT_BPS", 300.0),
        "max_holding_minutes": as_float("AUTO_PROFILE_MAX_HOLDING_MINUTES", 90.0),
        "exit_rationale": "runtime paper-only autonomy bootstrap; operator may override with explicit Agent thesis/profile",
    },
    "provenance": {
        "source": "scripts/start-auto-paper.sh",
        "generated_by": "default_runtime_paper_autonomy_profile",
        "runtime_file": True,
    },
    "safety": {
        "public_readonly_market_data": True,
        "public_readonly": True,
        "paper_or_watch_only": True,
        "real_orders": False,
        "signed_requests": False,
        "reads_api_keys": False,
        "binance_account_state": False,
    },
    "limitations": [
        "paper/watch only",
        "bootstrap autonomy profile is local runtime config, not a real order instruction",
        "no Binance credentials, signed requests, account reads, or real orders",
    ],
}
max_concurrent = os.environ.get("AUTO_PROFILE_MAX_CONCURRENT_PER_SYMBOL", "").strip()
if max_concurrent:
    try:
        parsed = int(max_concurrent)
    except ValueError:
        parsed = 0
    if parsed > 0:
        profile["paper_intent"]["max_concurrent_positions_per_symbol"] = parsed

path.write_text(json.dumps(profile, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
PY
}

ensure_strategy_state() {
  if [[ "$STRATEGY_REVIEW_ENABLED" == "0" || "$STRATEGY_REVIEW_ENABLED" == "false" || -z "$STRATEGY_STATE_PATH" ]]; then
    return 0
  fi
  mkdir -p "$(dirname "$STRATEGY_STATE_PATH")"
  local py
  py="$(python_bin)"
  if ! "$py" -m tradecat_auto.cli strategy-review \
    --ledger-path "$LEDGER_PATH" \
    --archive-path "$ARCHIVE_PATH" \
    --output-state-path "$STRATEGY_STATE_PATH" \
    --min-closed-positions "$STRATEGY_REVIEW_MIN_CLOSED_POSITIONS" \
    --max-open-positions "$STRATEGY_REVIEW_MAX_OPEN_POSITIONS" \
    --max-positions-per-symbol "$STRATEGY_REVIEW_MAX_POSITIONS_PER_SYMBOL" \
    --json >"$STRATEGY_REVIEW_REPORT_PATH"; then
    printf '{"event":"strategy_review_error","ts":"%s","ok":false,"path":"%s"}\n' "$(date -Iseconds)" "$STRATEGY_REVIEW_REPORT_PATH" >&2
  fi
}

run_cycle() {
  local py
  py="$(python_bin)"
  cd "$APP_DIR"
  export PYTHONPATH="$APP_DIR/src:${PYTHONPATH:-}"
  export PYTHONUNBUFFERED=1
  mkdir -p "$RUNTIME_DIR"
  ensure_paper_autonomy_profile
  ensure_strategy_state
  local -a sizing_args=()
  if [[ -n "$PAPER_MARGIN_BUDGET_USDT" ]]; then
    sizing_args+=(--paper-margin-budget-usdt "$PAPER_MARGIN_BUDGET_USDT")
  fi
  if [[ -n "$AGENT_MARGIN_USDT" ]]; then
    sizing_args+=(--agent-margin-usdt "$AGENT_MARGIN_USDT")
  fi
  if [[ -n "$EFFECTIVE_NOTIONAL_USDT" ]]; then
    sizing_args+=(--notional-usdt "$EFFECTIVE_NOTIONAL_USDT")
  fi
  if [[ -n "$PAPER_LEVERAGE" ]]; then
    sizing_args+=(--paper-leverage "$PAPER_LEVERAGE")
  fi
  if [[ -n "$AGENT_TRADE_THESIS_PATH" ]]; then
    sizing_args+=(--agent-trade-thesis-path "$AGENT_TRADE_THESIS_PATH")
  fi
  if [[ -n "$PAPER_AUTONOMY_PROFILE_PATH" ]]; then
    sizing_args+=(--paper-autonomy-profile-path "$PAPER_AUTONOMY_PROFILE_PATH")
  fi
  if [[ -n "$PORTFOLIO_RISK_POLICY_PATH" ]]; then
    sizing_args+=(--portfolio-risk-policy-path "$PORTFOLIO_RISK_POLICY_PATH")
  fi
  if [[ -n "$PAPER_KILL_SWITCH_PATH" ]]; then
    sizing_args+=(--paper-kill-switch-path "$PAPER_KILL_SWITCH_PATH")
  fi
  if [[ "$STRATEGY_REVIEW_ENABLED" != "0" && "$STRATEGY_REVIEW_ENABLED" != "false" && -n "$STRATEGY_STATE_PATH" ]]; then
    sizing_args+=(--strategy-state-path "$STRATEGY_STATE_PATH")
  fi
  printf '{"event":"cycle_start","ts":"%s"}\n' "$(date -Iseconds)"
  local -a command=(
  "$py" -m tradecat_auto.cli run-loop \
    --mode paper \
    "${sizing_args[@]}" \
    --state-path "$STATE_PATH" \
    --ledger-path "$LEDGER_PATH" \
    --archive-path "$ARCHIVE_PATH" \
    --journal-path "$JOURNAL_PATH" \
    --initial-balance-usdt "$INITIAL_BALANCE_USDT" \
    --paper-fee-bps "$PAPER_FEE_BPS" \
    --paper-slippage-bps "$PAPER_SLIPPAGE_BPS" \
    --paper-max-holding-minutes "$PAPER_MAX_HOLDING_MINUTES" \
    --interval-seconds "$INTERVAL_SECONDS" \
    --maintenance-interval-seconds "$MAINTENANCE_INTERVAL_SECONDS" \
    --event-limit "$EVENT_LIMIT" \
    --anomaly-limit "$ANOMALY_LIMIT" \
    --symbol "$SYMBOL" \
    --base-url "$BASE_URL" \
    --once \
    --json
  )
  if [[ -n "$MAX_EVENT_AGE_SECONDS" ]]; then
    command+=(--max-event-age-seconds "$MAX_EVENT_AGE_SECONDS")
  fi
  if command -v timeout >/dev/null 2>&1; then
    timeout --kill-after=10s "$CYCLE_TIMEOUT_SECONDS" "${command[@]}"
  else
    "${command[@]}"
  fi
}

run_forever() {
  mkdir -p "$RUNTIME_DIR"
  write_own_pid_file
  trap 'clear_own_pid_file; exit 0' INT TERM
  trap 'clear_own_pid_file' EXIT
  while true; do
    if ! run_cycle; then
      printf '{"event":"cycle_error","ts":"%s","ok":false}\n' "$(date -Iseconds)"
    fi
    sleep "$INTERVAL_SECONDS"
  done
}

start() {
  local action="${1:-start}"
  if is_running; then
    local pid
    pid="$(read_pid)"
    load_running_env_config "$pid"
    emit_text_or_json "$action" "running" "1" "1" "already_running" "running pid=$pid ledger=$LEDGER_PATH log=$LOG_FILE" "$pid"
    return 0
  fi
  rm -f "$PID_FILE"
  mkdir -p "$RUNTIME_DIR"
  (
    cd "$APP_DIR"
    if command -v setsid >/dev/null 2>&1; then
      setsid bash "$APP_DIR/scripts/start-auto-paper.sh" _run </dev/null >>"$LOG_FILE" 2>&1 &
    else
      nohup bash "$APP_DIR/scripts/start-auto-paper.sh" _run </dev/null >>"$LOG_FILE" 2>&1 &
    fi
    echo "$!" >"$PID_FILE"
  )
  local pid
  pid="$(read_pid)"
  emit_text_or_json "$action" "running" "1" "1" "started" "started pid=$pid ledger=$LEDGER_PATH archive=$ARCHIVE_PATH log=$LOG_FILE" "$pid"
}

stop() {
  if ! is_running; then
    rm -f "$PID_FILE"
    emit_text_or_json "stop" "stopped" "0" "1" "already_stopped" "stopped"
    return 0
  fi
  local pid
  pid="$(read_pid)"
  kill "$pid" >/dev/null 2>&1 || true
  for _ in $(seq 1 30); do
    if ! kill -0 "$pid" >/dev/null 2>&1; then
      rm -f "$PID_FILE"
      emit_text_or_json "stop" "stopped" "0" "1" "stopped" "stopped" "$pid"
      return 0
    fi
    sleep 0.2
  done
  kill -9 "$pid" >/dev/null 2>&1 || true
  rm -f "$PID_FILE"
  emit_text_or_json "stop" "stopped" "0" "1" "killed" "killed pid=$pid" "$pid"
}

status() {
  if is_running; then
    local pid
    pid="$(read_pid)"
    load_running_env_config "$pid"
    emit_text_or_json "status" "running" "1" "1" "running" "running pid=$pid ledger=$LEDGER_PATH log=$LOG_FILE" "$pid"
  else
    emit_text_or_json "status" "not_running" "0" "0" "not_running" "not running ledger=$LEDGER_PATH log=$LOG_FILE" "" "paper_service_not_running" "auto paper run-loop is not running"
    return 1
  fi
}

run_health_report() {
  local py
  py="$(python_bin)"
  cd "$APP_DIR"
  export PYTHONPATH="$APP_DIR/src:${PYTHONPATH:-}"
  "$py" -m tradecat_auto.cli health-report \
    --state-path "$STATE_PATH" \
    --ledger-path "$LEDGER_PATH" \
    --archive-path "$ARCHIVE_PATH" \
    --journal-path "$JOURNAL_PATH" \
    --max-heartbeat-age-seconds "$MAX_HEARTBEAT_AGE_SECONDS" \
    $(json_flag_args)
}

run_daily_report() {
  local py
  py="$(python_bin)"
  cd "$APP_DIR"
  export PYTHONPATH="$APP_DIR/src:${PYTHONPATH:-}"
  "$py" -m tradecat_auto.cli daily-report \
    --ledger-path "$LEDGER_PATH" \
    --archive-path "$ARCHIVE_PATH" \
    $(json_flag_args)
}

run_alert_payload() {
  local py
  py="$(python_bin)"
  cd "$APP_DIR"
  export PYTHONPATH="$APP_DIR/src:${PYTHONPATH:-}"
  "$py" -m tradecat_auto.cli alert-payload \
    --kind daily \
    --ledger-path "$LEDGER_PATH" \
    --archive-path "$ARCHIVE_PATH" \
    $(json_flag_args)
}

ops_check() {
  local py
  py="$(python_bin)"
  AUTO_OPS_RUNTIME_DIR="$RUNTIME_DIR" \
  AUTO_OPS_STATE_PATH="$STATE_PATH" \
  AUTO_OPS_LEDGER_PATH="$LEDGER_PATH" \
  AUTO_OPS_ARCHIVE_PATH="$ARCHIVE_PATH" \
  AUTO_OPS_JOURNAL_PATH="$JOURNAL_PATH" \
  AUTO_OPS_LOG_FILE="$LOG_FILE" \
  AUTO_OPS_PID_FILE="$PID_FILE" \
  AUTO_OPS_PYTHON="$py" \
  AUTO_OPS_APP_DIR="$APP_DIR" \
  AUTO_OPS_BASE_URL="$BASE_URL" \
  AUTO_OPS_SYSTEMCTL_BIN="$SYSTEMCTL_BIN" \
  AUTO_OPS_SYSTEMD_USER_DIR="$SYSTEMD_USER_DIR" \
  AUTO_OPS_SYSTEMD_SERVICE_UNIT="$SYSTEMD_SERVICE_UNIT" \
  AUTO_OPS_SYSTEMD_TIMER_UNIT="$SYSTEMD_TIMER_UNIT" \
  AUTO_OPS_CYCLE_TIMEOUT_SECONDS="$CYCLE_TIMEOUT_SECONDS" \
  AUTO_OPS_MIN_FREE_BYTES="$MIN_FREE_BYTES" \
  AUTO_OPS_MIN_NOFILE="$MIN_NOFILE" \
  AUTO_OPS_MIN_NPROC="$MIN_NPROC" \
  AUTO_OPS_LIMIT_NOFILE="$LIMIT_NOFILE" \
  AUTO_OPS_TASKS_MAX="$TASKS_MAX" \
  AUTO_OPS_RESTART_SEC="$RESTART_SEC" \
  AUTO_OPS_START_LIMIT_BURST="$START_LIMIT_BURST" \
  AUTO_OPS_START_LIMIT_INTERVAL_SECONDS="$START_LIMIT_INTERVAL_SECONDS" \
  AUTO_OPS_TIMEOUT_START_SECONDS="$TIMEOUT_START_SECONDS" \
  "$py" - <<'PY'
import json
import os
import re
import shutil
from datetime import UTC, datetime
from pathlib import Path

try:
    import resource
except ImportError:  # pragma: no cover - Windows fallback
    resource = None


def as_int(value: str, default: int) -> int:
    try:
        return int(float(value))
    except (TypeError, ValueError):
        return default


def is_under(child: Path, parent: Path) -> bool:
    try:
        child.resolve(strict=False).relative_to(parent.resolve(strict=False))
    except ValueError:
        return False
    return True


def check(checks, check_id, ok, severity, message, **extra):
    item = {"id": check_id, "ok": bool(ok), "severity": severity, "message": message}
    item.update(extra)
    checks.append(item)


runtime_dir = Path(os.environ["AUTO_OPS_RUNTIME_DIR"])
state_path = Path(os.environ["AUTO_OPS_STATE_PATH"])
ledger_path = Path(os.environ["AUTO_OPS_LEDGER_PATH"])
archive_path = Path(os.environ["AUTO_OPS_ARCHIVE_PATH"])
journal_path = Path(os.environ["AUTO_OPS_JOURNAL_PATH"])
log_file = Path(os.environ["AUTO_OPS_LOG_FILE"])
pid_file = Path(os.environ["AUTO_OPS_PID_FILE"])
app_dir = Path(os.environ["AUTO_OPS_APP_DIR"])
python_bin = Path(os.environ["AUTO_OPS_PYTHON"])
base_url = os.environ["AUTO_OPS_BASE_URL"]
systemctl_bin = os.environ["AUTO_OPS_SYSTEMCTL_BIN"]
min_free_bytes = as_int(os.environ["AUTO_OPS_MIN_FREE_BYTES"], 104857600)
min_nofile = as_int(os.environ["AUTO_OPS_MIN_NOFILE"], 1024)
min_nproc = as_int(os.environ["AUTO_OPS_MIN_NPROC"], 128)
checks = []

runtime_parent = runtime_dir.parent if runtime_dir.parent != Path("") else Path(".")
runtime_parent_exists = runtime_parent.exists()
free_bytes = shutil.disk_usage(runtime_parent if runtime_parent_exists else app_dir).free
check(checks, "runtime_parent_exists", runtime_parent_exists, "block", "runtime parent directory exists", path=str(runtime_parent))
check(checks, "runtime_parent_writable", os.access(runtime_parent if runtime_parent_exists else app_dir, os.W_OK), "block", "runtime parent is writable", path=str(runtime_parent))
check(checks, "disk_free_bytes", free_bytes >= min_free_bytes, "block", "runtime filesystem has enough free bytes", free_bytes=free_bytes, min_free_bytes=min_free_bytes)
for path_id, path in {
    "state_path_under_runtime": state_path,
    "ledger_path_under_runtime": ledger_path,
    "archive_path_under_runtime": archive_path,
    "journal_path_under_runtime": journal_path,
    "log_file_under_runtime": log_file,
    "pid_file_under_runtime": pid_file,
}.items():
    check(checks, path_id, is_under(path, runtime_dir), "block", f"{path_id} stays inside runtime_dir", path=str(path))

check(checks, "python_available", python_bin.exists() or shutil.which(str(python_bin)) is not None, "block", "python executable is available", python=str(python_bin))
check(checks, "project_source_exists", (app_dir / "src" / "tradecat_auto").exists(), "block", "tradecat_auto source tree exists", path=str(app_dir / "src" / "tradecat_auto"))
check(checks, "base_url_https", base_url.startswith("https://"), "block", "base_url uses https public endpoint", base_url=base_url)
check(checks, "systemctl_available", shutil.which(systemctl_bin) is not None, "warn", "systemctl is available for user service install", systemctl=systemctl_bin)

uid = getattr(os, "geteuid", lambda: None)()
check(
    checks,
    "identity_detected",
    True,
    "info",
    "process identity detected; root is allowed only by operator policy, non-root is safer for public paper/watch",
    uid=uid,
    run_as_root=uid == 0,
)

credential_env_names = sorted(
    key for key in os.environ
    if re.search(r"BINANCE_.*(?:KEY|SECRET|SIGNATURE|LISTEN)", key, flags=re.IGNORECASE)
)
check(
    checks,
    "no_binance_credential_env_names",
    not credential_env_names,
    "block",
    "environment does not expose Binance credential-like variable names to this public paper process",
    env_names=credential_env_names,
)

if resource is not None:
    nofile_soft, nofile_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
    check(checks, "nofile_limit", nofile_soft >= min_nofile, "warn", "file descriptor soft limit is high enough", soft=nofile_soft, hard=nofile_hard, min_soft=min_nofile)
    if hasattr(resource, "RLIMIT_NPROC"):
        nproc_soft, nproc_hard = resource.getrlimit(resource.RLIMIT_NPROC)
        nproc_ok = nproc_soft == resource.RLIM_INFINITY or nproc_soft >= min_nproc
        check(checks, "process_task_limit", nproc_ok, "warn", "process/task soft limit is high enough", soft=nproc_soft, hard=nproc_hard, min_soft=min_nproc)
else:
    check(checks, "resource_limits_available", True, "info", "resource module unavailable; skip OS limit checks")

pid_text = ""
try:
    pid_text = pid_file.read_text(encoding="utf-8").strip()
except OSError:
    pass
check(checks, "single_pid_file", not pid_text or pid_text.isdigit(), "block", "pid file is absent or contains one numeric pid", pid=pid_text or None)

blocking = [item for item in checks if item["severity"] == "block" and not item["ok"]]
payload = {
    "schema": "tradecat_auto.paper_ops_report.v1",
    "schema_version": "1.0.0",
    "ok": not blocking,
    "mode": "paper",
    "command": "auto-paper-service",
    "action": "ops-check",
    "generated_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
    "dependency_chain": [
        "Hermes/operator supervisor",
        "tradecat-public repository-root Python project",
        "embedded Skill package at skills/tradecat-public",
        "root .venv Python environment",
        "public online sheet signal source",
        "Agent-supplied Binance public-readonly market context",
        "context-audit",
        "Agent trade thesis with explicit paper sizing and exits; optional user-supplied paper autonomy profile/policy",
        "portfolio risk policy / paper kill switch",
        "auto-paper run-loop",
        "paper ledger / cycle archive / audit journal",
        "latest-cycle / latest-decision / health-report / daily-report / alert-payload",
    ],
    "runtime": {
        "runtime_dir": str(runtime_dir),
        "state_path": str(state_path),
        "ledger_path": str(ledger_path),
        "archive_path": str(archive_path),
        "journal_path": str(journal_path),
        "log_file": str(log_file),
        "pid_file": str(pid_file),
    },
    "systemd": {
        "user_dir": os.environ["AUTO_OPS_SYSTEMD_USER_DIR"],
        "service_unit": os.environ["AUTO_OPS_SYSTEMD_SERVICE_UNIT"],
        "timer_unit": os.environ["AUTO_OPS_SYSTEMD_TIMER_UNIT"],
        "lifecycle_owner": "service",
        "legacy_timer_policy": "disabled_on_install",
        "cycle_timeout_seconds": as_int(os.environ["AUTO_OPS_CYCLE_TIMEOUT_SECONDS"], 6000),
        "start_limit_burst": as_int(os.environ["AUTO_OPS_START_LIMIT_BURST"], 5),
        "start_limit_interval_seconds": as_int(os.environ["AUTO_OPS_START_LIMIT_INTERVAL_SECONDS"], 600),
        "restart_sec": os.environ["AUTO_OPS_RESTART_SEC"],
        "timeout_start_seconds": as_int(os.environ["AUTO_OPS_TIMEOUT_START_SECONDS"], 120),
        "limit_nofile": as_int(os.environ["AUTO_OPS_LIMIT_NOFILE"], 4096),
        "tasks_max": as_int(os.environ["AUTO_OPS_TASKS_MAX"], 64),
    },
    "checks": checks,
    "blocking_checks": [item["id"] for item in blocking],
    "operations": {
        "lifecycle": "systemd user service owns one long-running paper loop; legacy timer is disabled on install",
        "restart_storm": "systemd StartLimitBurst/StartLimitIntervalSec and RestartSec bound retry pressure",
        "identity": "uid/run_as_root is reported; public paper/watch does not require Binance credentials",
        "logging_audit": "log_file plus paper_audit.sqlite3 preserve service behavior and audit chain",
        "health": "health-report detects heartbeat_stale, ledger/archive/audit failures, process-alive-but-stale cases, and bounded cycle timeouts",
        "dependencies": "ops-check validates Python, paths, disk, limits, systemctl availability, and credential env names",
        "rollback": "use git checkout/tag outside runtime; runtime ledger/archive remain isolated under .runtime",
    },
    "safety": {
        "public_readonly_market_data": True,
        "public_readonly": True,
        "paper_or_watch_only": True,
        "real_orders": False,
        "signed_requests": False,
        "reads_api_keys": False,
        "binance_account_state": False,
    },
}
if blocking:
    payload["error"] = {
        "code": "paper_ops_preflight_failed",
        "kind": "configuration",
        "message": "one or more blocking auto-paper ops checks failed",
        "retryable": False,
    }
print(json.dumps(payload, ensure_ascii=False))
raise SystemExit(0 if payload["ok"] else 1)
PY
}

heal() {
  if ! is_running; then
    start "heal"
    return $?
  fi
  local tmp_health
  local previous_json="$JSON"
  local py
  py="$(python_bin)"
  tmp_health="$(mktemp)"
  JSON=1
  if ! run_health_report >"$tmp_health" 2>/dev/null; then
    JSON="$previous_json"
    rm -f "$tmp_health"
    restart
    return $?
  fi
  JSON="$previous_json"
  if "$py" - "$tmp_health" <<'PY' >/dev/null 2>&1; then
import json
import sys
payload = json.loads(open(sys.argv[1], encoding="utf-8").read())
alerts = set(payload.get("alerts") or [])
raise SystemExit(0 if payload.get("ok") and "heartbeat_stale" not in alerts else 1)
PY
    rm -f "$tmp_health"
    status
  else
    rm -f "$tmp_health"
    restart
  fi
}

restart() {
  if [[ "$JSON" -eq 1 ]]; then
    stop >/dev/null || true
    start "restart"
    return $?
  fi
  stop
  start
}

write_systemd_units() {
  mkdir -p "$SYSTEMD_USER_DIR" "$RUNTIME_DIR"
  local service_path="$SYSTEMD_USER_DIR/$SYSTEMD_SERVICE_UNIT"
  local timer_path="$SYSTEMD_USER_DIR/$SYSTEMD_TIMER_UNIT"
  {
    cat <<UNIT
[Unit]
Description=TradeCat auto paper trading cycle (public data, paper only)
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=$START_LIMIT_INTERVAL_SECONDS
StartLimitBurst=$START_LIMIT_BURST

[Service]
Type=simple
WorkingDirectory=$APP_DIR
TimeoutStartSec=$TIMEOUT_START_SECONDS
TimeoutStopSec=30
Restart=on-failure
RestartSec=$RESTART_SEC
Environment=PYTHONUNBUFFERED=1
Environment=PYTHONPATH=$APP_DIR/src
Environment=TRADECAT_AUTO_PAPER_RUNTIME_DIR=$RUNTIME_DIR
Environment=TRADECAT_AUTO_PAPER_STATE_PATH=$STATE_PATH
Environment=TRADECAT_AUTO_PAPER_LEDGER_PATH=$LEDGER_PATH
Environment=TRADECAT_AUTO_PAPER_ARCHIVE_PATH=$ARCHIVE_PATH
Environment=TRADECAT_AUTO_PAPER_JOURNAL_PATH=$JOURNAL_PATH
Environment=TRADECAT_AUTO_PAPER_LOG_FILE=$LOG_FILE
Environment=TRADECAT_AUTO_PAPER_INTERVAL_SECONDS=$INTERVAL_SECONDS
Environment=TRADECAT_AUTO_PAPER_MAINTENANCE_INTERVAL_SECONDS=$MAINTENANCE_INTERVAL_SECONDS
Environment=TRADECAT_AUTO_PAPER_CYCLE_TIMEOUT_SECONDS=$CYCLE_TIMEOUT_SECONDS
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_ENABLED=$PAPER_AUTONOMY_ENABLED
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_MARGIN_USDT=$PAPER_AUTONOMY_MARGIN_USDT
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_LEVERAGE=$PAPER_AUTONOMY_LEVERAGE
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_STOP_LOSS_BPS=$PAPER_AUTONOMY_STOP_LOSS_BPS
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_TAKE_PROFIT_BPS=$PAPER_AUTONOMY_TAKE_PROFIT_BPS
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_MAX_HOLDING_MINUTES=$PAPER_AUTONOMY_MAX_HOLDING_MINUTES
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_DIRECTION_POLICY=$PAPER_AUTONOMY_DIRECTION_POLICY
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_MIN_SIGNAL_SCORE=$PAPER_AUTONOMY_MIN_SIGNAL_SCORE
Environment=TRADECAT_AUTO_PAPER_AUTONOMY_ALLOW_MULTIPLE=$PAPER_AUTONOMY_ALLOW_MULTIPLE
UNIT
    if [[ -n "$PAPER_AUTONOMY_MAX_CONCURRENT_PER_SYMBOL" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_AUTONOMY_MAX_CONCURRENT_PER_SYMBOL=%s\n' "$PAPER_AUTONOMY_MAX_CONCURRENT_PER_SYMBOL"
    fi
    local proxy_key proxy_value
    for proxy_key in HTTP_PROXY HTTPS_PROXY ALL_PROXY NO_PROXY http_proxy https_proxy all_proxy no_proxy; do
      proxy_value="${!proxy_key:-}"
      if [[ -n "$proxy_value" ]]; then
        printf 'Environment=%s=%s\n' "$proxy_key" "$proxy_value"
      fi
    done
    if [[ -n "$PAPER_MARGIN_BUDGET_USDT" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_MARGIN_BUDGET_USDT=%s\n' "$PAPER_MARGIN_BUDGET_USDT"
    fi
    if [[ -n "$AGENT_MARGIN_USDT" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_AGENT_MARGIN_USDT=%s\n' "$AGENT_MARGIN_USDT"
    fi
    if [[ -n "$EFFECTIVE_NOTIONAL_USDT" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_EFFECTIVE_NOTIONAL_USDT=%s\n' "$EFFECTIVE_NOTIONAL_USDT"
    fi
    if [[ -n "$PAPER_LEVERAGE" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_LEVERAGE=%s\n' "$PAPER_LEVERAGE"
    fi
    if [[ -n "$AGENT_TRADE_THESIS_PATH" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_AGENT_TRADE_THESIS_PATH=%s\n' "$AGENT_TRADE_THESIS_PATH"
    fi
    if [[ -n "$PAPER_AUTONOMY_PROFILE_PATH" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_AUTONOMY_PROFILE_PATH=%s\n' "$PAPER_AUTONOMY_PROFILE_PATH"
    fi
    cat <<UNIT
Environment=TRADECAT_AUTO_PAPER_INITIAL_BALANCE_USDT=$INITIAL_BALANCE_USDT
Environment=TRADECAT_AUTO_PAPER_FEE_BPS=$PAPER_FEE_BPS
Environment=TRADECAT_AUTO_PAPER_SLIPPAGE_BPS=$PAPER_SLIPPAGE_BPS
Environment=TRADECAT_AUTO_PAPER_MAX_HOLDING_MINUTES=$PAPER_MAX_HOLDING_MINUTES
Environment=TRADECAT_AUTO_PAPER_EVENT_LIMIT=$EVENT_LIMIT
Environment=TRADECAT_AUTO_PAPER_ANOMALY_LIMIT=$ANOMALY_LIMIT
Environment=TRADECAT_AUTO_PAPER_SYMBOL=$SYMBOL
Environment=TRADECAT_AUTO_PAPER_BASE_URL=$BASE_URL
Environment=TRADECAT_AUTO_PAPER_PORTFOLIO_RISK_POLICY_PATH=$PORTFOLIO_RISK_POLICY_PATH
Environment=TRADECAT_AUTO_PAPER_KILL_SWITCH_PATH=$PAPER_KILL_SWITCH_PATH
Environment=TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_ENABLED=$STRATEGY_REVIEW_ENABLED
Environment=TRADECAT_AUTO_PAPER_STRATEGY_STATE_PATH=$STRATEGY_STATE_PATH
Environment=TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_REPORT_PATH=$STRATEGY_REVIEW_REPORT_PATH
Environment=TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_MIN_CLOSED_POSITIONS=$STRATEGY_REVIEW_MIN_CLOSED_POSITIONS
Environment=TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_MAX_OPEN_POSITIONS=$STRATEGY_REVIEW_MAX_OPEN_POSITIONS
Environment=TRADECAT_AUTO_PAPER_STRATEGY_REVIEW_MAX_POSITIONS_PER_SYMBOL=$STRATEGY_REVIEW_MAX_POSITIONS_PER_SYMBOL
UNIT
    if [[ -n "$MAX_EVENT_AGE_SECONDS" ]]; then
      printf 'Environment=TRADECAT_AUTO_PAPER_MAX_EVENT_AGE_SECONDS=%s\n' "$MAX_EVENT_AGE_SECONDS"
    fi
    cat <<UNIT
ExecStart=$APP_DIR/scripts/start-auto-paper.sh _run
StandardOutput=append:$LOG_FILE
StandardError=append:$LOG_FILE
UMask=0077
LimitNOFILE=$LIMIT_NOFILE
TasksMax=$TASKS_MAX
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
LockPersonality=true
RestrictSUIDSGID=true

[Install]
WantedBy=default.target
UNIT
  } >"$service_path"
  rm -f "$timer_path"
}

systemd_install() {
  "$SYSTEMCTL_BIN" --user disable --now "$SYSTEMD_TIMER_UNIT" >/dev/null 2>&1 || true
  "$SYSTEMCTL_BIN" --user stop "$SYSTEMD_SERVICE_UNIT" >/dev/null 2>&1 || true
  stop_running_loop_quietly
  write_systemd_units
  if ! "$SYSTEMCTL_BIN" --user daemon-reload; then
    emit_text_or_json "systemd-install" "failed" "0" "0" "daemon_reload_failed" "systemd daemon-reload failed" "" "systemd_daemon_reload_failed" "systemctl --user daemon-reload failed"
    return 1
  fi
  "$SYSTEMCTL_BIN" --user reset-failed "$SYSTEMD_SERVICE_UNIT" >/dev/null 2>&1 || true
  if ! "$SYSTEMCTL_BIN" --user enable --now "$SYSTEMD_SERVICE_UNIT"; then
    emit_text_or_json "systemd-install" "failed" "0" "0" "enable_failed" "systemd service enable failed" "" "systemd_service_enable_failed" "systemctl --user enable --now $SYSTEMD_SERVICE_UNIT failed"
    return 1
  fi
  local active=0
  for _ in $(seq 1 20); do
    if "$SYSTEMCTL_BIN" --user is-active --quiet "$SYSTEMD_SERVICE_UNIT"; then
      active=1
      break
    fi
    sleep 0.25
  done
  if [[ "$active" != "1" ]]; then
    emit_text_or_json "systemd-install" "failed" "0" "0" "service_not_active" "systemd service is not active after enable --now" "" "systemd_service_not_active" "systemctl --user is-active --quiet $SYSTEMD_SERVICE_UNIT failed"
    return 1
  fi
  local pid=""
  for _ in $(seq 1 10); do
    if is_running; then
      pid="$(read_pid)"
      load_running_env_config "$pid"
      break
    fi
    sleep 0.1
  done
  emit_text_or_json "systemd-install" "enabled" "1" "1" "service_enabled" "installed and enabled $SYSTEMD_SERVICE_UNIT in $SYSTEMD_USER_DIR" "$pid"
}

systemd_uninstall() {
  mkdir -p "$SYSTEMD_USER_DIR"
  "$SYSTEMCTL_BIN" --user disable --now "$SYSTEMD_SERVICE_UNIT" >/dev/null 2>&1 || true
  "$SYSTEMCTL_BIN" --user disable --now "$SYSTEMD_TIMER_UNIT" >/dev/null 2>&1 || true
  rm -f "$SYSTEMD_USER_DIR/$SYSTEMD_SERVICE_UNIT" "$SYSTEMD_USER_DIR/$SYSTEMD_TIMER_UNIT"
  "$SYSTEMCTL_BIN" --user daemon-reload >/dev/null 2>&1 || true
  emit_text_or_json "systemd-uninstall" "disabled" "0" "1" "service_disabled" "disabled and removed $SYSTEMD_SERVICE_UNIT and legacy $SYSTEMD_TIMER_UNIT from $SYSTEMD_USER_DIR"
}

case "$ACTION" in
  start) start ;;
  stop) stop ;;
  restart) restart ;;
  status) status ;;
  ops-check) ops_check ;;
  heal) heal ;;
  health) run_health_report ;;
  daily) run_daily_report ;;
  alert) run_alert_payload ;;
  systemd-install) systemd_install ;;
  systemd-uninstall) systemd_uninstall ;;
  _run) run_forever ;;
  _cycle) run_cycle ;;
  *) echo "usage: $0 [--json] start|stop|restart|status|ops-check|heal|systemd-install|systemd-uninstall" >&2; exit 2 ;;
esac
