#!/usr/bin/env bash
# codex.sh — dispatch codex in one tested, fail-closed wrapper. Invoke by path.
# Exit: 0 = completion, 124 = timeout/killed, 2 = usage error, 75 = rate-limited, 3 = engine down OR agent
#   returned 0 with empty captured output (silent write failure / disk full — success requires evidence).
set -uo pipefail

WORKSPACE="" PROMPT="" TASK_SLUG="" MODEL="" THREAD_ID="" PROFILE="" TIMEOUT=1800 PERMISSION_MODE="" PERMISSION_MODE_SEEN=0
MODE="dispatch"
while [[ $# -gt 0 ]]; do
  case "$1" in
    --workspace) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--workspace requires a value"}' >&2; exit 2; }; WORKSPACE="${2:-}"; shift 2;;
    --trust) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--trust requires a value"}' >&2; exit 2; }; PROMPT="${2:-}"; shift 2;;
    --task-slug) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--task-slug requires a value"}' >&2; exit 2; }; TASK_SLUG="${2:-}"; shift 2;;
    --model) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--model requires a value"}' >&2; exit 2; }; MODEL="${2:-}"; shift 2;;
    --thread-id) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--thread-id requires a value"}' >&2; exit 2; }; THREAD_ID="${2:-}"; shift 2;;
    --timeout) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--timeout requires a value"}' >&2; exit 2; }; TIMEOUT="${2:-}"; shift 2;;
    --profile) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--profile requires a value"}' >&2; exit 2; }; PROFILE="${2:-}"; shift 2;;
    --permission-mode) [[ $# -lt 2 || -z "${2:-}" || $PERMISSION_MODE_SEEN -eq 1 ]] && { echo '{"ok":false,"detail":"--permission-mode requires one value"}' >&2; exit 2; }; PERMISSION_MODE="$2"; PERMISSION_MODE_SEEN=1; shift 2;;
    --health) MODE="health"; shift;;
    --list-models) MODE="list-models"; shift;;
    *) echo "{\"ok\":false,\"detail\":\"unknown flag: $1\"}" >&2; exit 2;;
  esac
done

[[ $PERMISSION_MODE_SEEN -eq 0 ]] && PERMISSION_MODE="safe"
[[ "$PERMISSION_MODE" == "safe" || "$PERMISSION_MODE" == "unsafe" ]] || { echo '{"ok":false,"detail":"--permission-mode must be safe or unsafe"}' >&2; exit 2; }
INCIDENT_SAFE=0
[[ "$TASK_SLUG" == incident-* ]] && INCIDENT_SAFE=1
[[ $INCIDENT_SAFE -eq 0 || "$PERMISSION_MODE" == "safe" ]] || { echo '{"ok":false,"detail":"incident dispatch requires --permission-mode safe"}' >&2; exit 2; }

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
source "$SCRIPT_DIR/lib/finalize.sh"
source "$SCRIPT_DIR/lib/spend-cap.sh"
_CODEX_BIN=""

load_env() {
  local env_candidates=(
    "${_CODEX_ENV:-}"
    "$REPO_ROOT/workflows/lib/CODEX.env"
    "$REPO_ROOT/CODEX.env"
    "$HOME/.claude/workflows/lib/CODEX.env"
  )
  local env_file
  for env_file in "${env_candidates[@]}"; do
    [[ -n "$env_file" && -f "$env_file" ]] || continue
    source "$env_file" || return 1
    return 0
  done
  return 0
}

resolve_codex_bin() {
  local requested="${_CODEX_ENGINE_BIN:-cdx}"
  local candidate

  if [[ "$requested" == */* ]]; then
    [[ -x "$requested" ]] || return 1
    printf '%s\n' "$requested"
    return 0
  fi

  candidate="$(command -v "$requested" 2>/dev/null || true)"
  if [[ -n "$candidate" && -x "$candidate" ]]; then
    printf '%s\n' "$candidate"
    return 0
  fi

  for candidate in "$HOME/.local/bin/cdx" "$HOME/.claude/bin/codex"; do
    if [[ -x "$candidate" ]]; then
      printf '%s\n' "$candidate"
      return 0
    fi
  done

  return 1
}

make_log_dir() {
  local dir="${HARNESS_LOG_DIR:-$HOME/Projects/mega-plan-harness/tmp/logs}"
  if ! mkdir -p "$dir" 2>/dev/null || [[ ! -d "$dir" || ! -w "$dir" ]]; then
    dir="${TMPDIR:-/tmp}/mega-plan-harness-logs"
    mkdir -p "$dir" || return 1
  fi
  printf '%s\n' "$dir"
}

parse_model_effort() {
  MODEL_NAME=""
  MODEL_EFFORT=""
  python3 - "$1" <<'PY'
import re
import sys

value = sys.argv[1]
match = re.match(r"^(.*)-(high|medium|low|xhigh)$", value)
if match:
    print(match.group(1))
    print(match.group(2))
else:
    print(value)
    print("high")
PY
}

prepare_codex_engine() {
  if ! load_env; then
    echo '{"ok":false,"detail":"CODEX.env source failed — engine precondition unmet"}' >&2
    return 3
  fi
  # runplan's long-lived daemon may start with a minimal PATH. Keep both the
  # launcher and the real Codex binary discoverable in that environment.
  # Keep the launcher and real Codex binary discoverable, but bypass the global
  # agent shim: that shim creates a nested overlay whose commits cannot reach the
  # harness task worktree. The harness already provides the required isolation.
  export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
  _CODEX_BIN="$(resolve_codex_bin)" || {
    echo '{"ok":false,"detail":"codex executable not found"}' >&2
    return 3
  }
}

case "$MODE" in
  health)
    prepare_codex_engine || exit $?
    [[ -n "$PROFILE" ]] && _PROFILE_ARGS=("--profile=$PROFILE") || _PROFILE_ARGS=()
    version_out="$("$_CODEX_BIN" "${_PROFILE_ARGS[@]}" --version 2>/dev/null)" || { echo '{"ok":false,"detail":"codex --version failed"}' >&2; exit 3; }
    version_out="$(printf '%s' "$version_out" | tail -n 1 | tr -d '\r')"
    VERSION_OUT="$version_out" python3 - <<'PY'
import json
import os
print(json.dumps({"ok": True, "version": os.environ.get("VERSION_OUT", "")}, separators=(",", ":")))
PY
    exit 0
    ;;
  list-models)
    prepare_codex_engine || exit $?
    [[ -n "$PROFILE" ]] && _PROFILE_ARGS=("--profile=$PROFILE") || _PROFILE_ARGS=()
    "$_CODEX_BIN" "${_PROFILE_ARGS[@]}" models --json
    exit $?
    ;;
esac

TASK_SLUG="${TASK_SLUG//[^a-zA-Z0-9_-]/}"
[[ $INCIDENT_SAFE -eq 0 || "$PERMISSION_MODE" == "safe" ]] || { echo '{"ok":false,"detail":"incident dispatch requires --permission-mode safe"}' >&2; exit 2; }
RUN_ID="$(date +%Y-%m-%d-%H-%M-%S)-$$"

[[ -n "$WORKSPACE" && -d "$WORKSPACE" ]] || { echo '{"ok":false,"detail":"--workspace missing or not a dir"}' >&2; exit 2; }
[[ -n "$PROMPT" ]] || { echo '{"ok":false,"detail":"--trust (prompt) required"}' >&2; exit 2; }
[[ -n "$TASK_SLUG" ]] || { echo '{"ok":false,"detail":"--task-slug required (log + retry hygiene)"}' >&2; exit 2; }
[[ -n "$MODEL" ]] || { echo '{"ok":false,"detail":"--model required (no silent default)"}' >&2; exit 2; }

readarray -t MODEL_PARTS < <(parse_model_effort "$MODEL")
MODEL_NAME="${MODEL_PARTS[0]:-}"
MODEL_EFFORT="${MODEL_PARTS[1]:-}"
[[ -n "$MODEL_NAME" && -n "$MODEL_EFFORT" ]] || { echo '{"ok":false,"detail":"--model parse failed"}' >&2; exit 2; }

_SPEND_CAP_ACCOUNT="${PROFILE:-$(spend_cap_default_account || true)}"
[[ -n "$_SPEND_CAP_ACCOUNT" ]] && spend_cap_refuse_if_capped "$_SPEND_CAP_ACCOUNT" 'codex' "$THREAD_ID" codex "$MODEL"
prepare_codex_engine || exit $?

if [[ "${OD_DISPATCH_K3S:-}" != "1" && -f "$SCRIPT_DIR/lib/remote-seat.sh" ]]; then
  source "$SCRIPT_DIR/lib/remote-seat.sh"
  seat_remote_dispatch codex codex.sh "$WORKSPACE" "$PROMPT" "$TASK_SLUG" "$MODEL" "$TIMEOUT" "$PROFILE" "$PERMISSION_MODE" --thread-id "$THREAD_ID"
  seat_rc=$?
  [[ $seat_rc -eq 1 ]] || exit $seat_rc
fi

# Reaching here means the seat was NOT remoted and codex is about to run here. Inside a seat container that is the
# sanctioned execution plane. OD_DISPATCH_K3S is also sanctioned: this process is only the controller, and both its
# k3s path and its loud pre-agent podman fallback execute the model remotely.
# Headless callers exit 97 naming the remote path; a terminal on any std fd means the owner
# is driving and the dispatch proceeds.
DISPATCH_GUARD="$HOME/.claude/bin/local-dispatch-guard"
if [[ "${HARNESS_SEAT_CONTAINER:-}" == "1" || "${OD_DISPATCH_K3S:-}" == "1" ]]; then
  :
elif [[ -x "$DISPATCH_GUARD" ]]; then
  "$DISPATCH_GUARD" codex.sh || exit $?
elif [[ ! -t 0 && ! -t 1 && ! -t 2 ]]; then
  echo "{\"ok\":false,\"detail\":\"local-dispatch-guard missing at $DISPATCH_GUARD — refusing headless dispatch\"}" >&2
  exit 97
fi

LOG_DIR="$(make_log_dir)" || { echo '{"ok":false,"detail":"log dir create failed"}' >&2; exit 3; }
LOG="$LOG_DIR/$RUN_ID-$TASK_SLUG.log"
RAW_OUTPUT_FILE="$LOG_DIR/$RUN_ID-$TASK_SLUG.raw"
SIDECAR_PATH_FILE="$LOG_DIR/$RUN_ID-$TASK_SLUG.sidecar-path"
SIDECAR_PIDS_FILE="$LOG_DIR/$RUN_ID-$TASK_SLUG.sidecar-pids"
SIDECAR_FIFO="$LOG_DIR/$RUN_ID-$TASK_SLUG.sidecar.fifo"
CHILD_RC_FILE="$LOG_DIR/$RUN_ID-$TASK_SLUG.child-rc"
TERMINAL_REAPED_FILE="$LOG_DIR/$RUN_ID-$TASK_SLUG.terminal-reaped"
TERMINAL_GRACE_SECS="${CODEX_TERMINAL_GRACE_SECS:-2}"
TRANSCRIPT_PATH="${HARNESS_TRANSCRIPT_PATH:-}"

# codex normally exits after turn.completed; when it does not, the wrapper ends it
# here. The orchestrator's backstop signals the whole process group, which would take
# this wrapper down before its epilogue can print the contract status line.
reap_after_terminal_event() {
  local child_pid="$1"
  while kill -0 "$child_pid" 2>/dev/null; do
    if grep -q '"type":"turn.completed"' "$RAW_OUTPUT_FILE" 2>/dev/null; then
      : > "$TERMINAL_REAPED_FILE"
      sleep "$TERMINAL_GRACE_SECS"
      kill -TERM "$child_pid" 2>/dev/null || true
      sleep 2
      kill -KILL "$child_pid" 2>/dev/null || true
      return
    fi
    sleep 0.5
  done
}
if [[ -n "$TRANSCRIPT_PATH" ]]; then
  mkdir -p "$(dirname "$TRANSCRIPT_PATH")" || { echo '{"ok":false,"detail":"transcript dir create failed"}' >&2; exit 3; }
fi

stream_output() {
  if [[ -n "$TRANSCRIPT_PATH" ]]; then
    tee -a "$LOG" "$RAW_OUTPUT_FILE" "$TRANSCRIPT_PATH"
  else
    tee -a "$LOG" "$RAW_OUTPUT_FILE"
  fi
}

emit_event_line() {
  local line="$1"
  if [[ -n "$TRANSCRIPT_PATH" ]]; then
    printf '%s\n' "$line" | tee -a "$LOG" "$TRANSCRIPT_PATH"
  else
    printf '%s\n' "$line" | tee -a "$LOG"
  fi
}

stream_child_output() {
  local line sidecar_path="" tail_pid="" stream_pid=""

  cleanup() {
    local waited=0
    if [[ -n "$tail_pid" ]]; then
      kill "$tail_pid" 2>/dev/null || true
      wait "$tail_pid" 2>/dev/null || true
    fi
    if [[ -n "$stream_pid" ]]; then
      # The reader blocks in open(2) on the fifo until a writer appears. When
      # tail dies before ever opening it, that open never returns, so drain on a
      # bound and then kill rather than waiting forever.
      while kill -0 "$stream_pid" 2>/dev/null && ((waited < 20)); do
        sleep 0.1
        waited=$((waited + 1))
      done
      kill "$stream_pid" 2>/dev/null || true
      wait "$stream_pid" 2>/dev/null || true
    fi
    rm -f "$SIDECAR_FIFO" "$SIDECAR_PIDS_FILE"
  }
  trap cleanup EXIT INT TERM

  while IFS= read -r line || [[ -n "$line" ]]; do
    line="${line%$'\r'}"
    printf '%s\n' "$line" | stream_output

    if [[ -z "$sidecar_path" && "$line" == /* && -f "$line" ]]; then
      sidecar_path="$line"
      printf '%s\n' "$sidecar_path" > "$SIDECAR_PATH_FILE"

      rm -f "$SIDECAR_FIFO"
      if mkfifo "$SIDECAR_FIFO" 2>/dev/null; then
        stream_output < "$SIDECAR_FIFO" &
        stream_pid=$!
        tail -n +1 -F "$sidecar_path" > "$SIDECAR_FIFO" 2>/dev/null &
        tail_pid=$!
        printf '%s %s\n' "$tail_pid" "$stream_pid" > "$SIDECAR_PIDS_FILE"
        if ! kill -0 "$tail_pid" 2>/dev/null; then
          kill "$stream_pid" 2>/dev/null || true
          wait "$stream_pid" 2>/dev/null || true
          rm -f "$SIDECAR_FIFO" "$SIDECAR_PIDS_FILE"
          tail_pid=""
          stream_pid=""
        fi
      fi
    fi
  done
}

cleanup_sidecar_tail() {
  local tail_pid="" stream_pid=""
  if [[ -f "$SIDECAR_PIDS_FILE" ]]; then
    read -r tail_pid stream_pid < "$SIDECAR_PIDS_FILE" || true
    [[ -n "$tail_pid" ]] && kill "$tail_pid" 2>/dev/null || true
    [[ -n "$stream_pid" ]] && kill "$stream_pid" 2>/dev/null || true
  fi
  rm -f "$SIDECAR_FIFO" "$SIDECAR_PIDS_FILE" "$SIDECAR_PATH_FILE" "$CHILD_RC_FILE" "$TERMINAL_REAPED_FILE"
}

# timeout(1) bounds the child, not the pipeline: a grandchild that survives the
# kill keeps the stdout pipe open, so an unbounded wait here never returns.
await_output_pipeline() {
  local watchdog
  (
    # killing this subshell does not reach its own sleep; the trap forwards.
    napper=""
    trap 'kill "$napper" 2>/dev/null; exit 0' TERM
    sleep $((TIMEOUT + 30)) &
    napper=$!
    wait "$napper" 2>/dev/null || exit 0
    kill -TERM "$OUTPUT_PIPELINE_PID" 2>/dev/null || true
    sleep 2
    kill -KILL "$OUTPUT_PIPELINE_PID" 2>/dev/null || true
    [[ -f "$CHILD_RC_FILE" ]] || printf '124\n' >"$CHILD_RC_FILE"
  ) &
  watchdog=$!
  wait "$OUTPUT_PIPELINE_PID" 2>/dev/null || true
  kill "$watchdog" 2>/dev/null || true
  wait "$watchdog" 2>/dev/null || true
}

trap cleanup_sidecar_tail EXIT
trap 'exit 143' INT TERM

# Safe mode always uses Codex's workspace-write sandbox. Dangerous bypass requires explicit unsafe mode.
CODEX_ARGS=(exec --json -C "$WORKSPACE" --skip-git-repo-check --model "$MODEL_NAME" -c "model_reasoning_effort=$MODEL_EFFORT")
[[ -n "$PROFILE" ]] && _CODEX_BIN_ARGS=("--profile=$PROFILE") || _CODEX_BIN_ARGS=()
if [[ "$PERMISSION_MODE" == "safe" ]]; then
  _CODEX_BIN_ARGS+=(--ask-for-approval never)
  CODEX_ARGS+=(--sandbox workspace-write)
else
  CODEX_ARGS+=(--dangerously-bypass-approvals-and-sandbox)
fi
[[ -n "$THREAD_ID" ]] && CODEX_ARGS+=(--thread-id "$THREAD_ID")
CODEX_ARGS+=("$PROMPT")

{
  echo "=== TOOL: codex (${MODEL_NAME}/${MODEL_EFFORT}) ==="
  echo "=== CMD: $(printf '%q ' "$_CODEX_BIN" "${_CODEX_BIN_ARGS[@]}" "${CODEX_ARGS[@]}") ==="
  echo "=== workspace: $WORKSPACE  timeout: ${TIMEOUT}s  log: $LOG ==="
  [[ -n "$THREAD_ID" ]] && echo "=== thread_id: $THREAD_ID ==="
} | tee "$LOG"

cd "$WORKSPACE" || { echo '{"ok":false,"detail":"cd workspace failed"}' >&2; exit 2; }
emit_event_line "$(MODEL="$MODEL" WORKSPACE="$WORKSPACE" LOG="$LOG" python3 - <<'PY'
import json
import os

print(json.dumps({
    "kind": "started",
    "model": os.environ["MODEL"],
    "workspace": os.environ["WORKSPACE"],
    "log": os.environ["LOG"],
}, separators=(",", ":")))
PY
)"

(
  timeout -k 5 "$TIMEOUT" "$_CODEX_BIN" "${_CODEX_BIN_ARGS[@]}" "${CODEX_ARGS[@]}" < /dev/null 2>&1 &
  child=$!
  reap_after_terminal_event "$child" &
  reaper=$!
  wait "$child"
  printf '%s\n' "$?" > "$CHILD_RC_FILE"
  kill "$reaper" 2>/dev/null || true
  wait "$reaper" 2>/dev/null || true
) | stream_child_output &
OUTPUT_PIPELINE_PID=$!
await_output_pipeline
RC="$(cat "$CHILD_RC_FILE" 2>/dev/null || printf '3')"
if [[ -f "$TERMINAL_REAPED_FILE" ]]; then RC=0; fi
raw_output="$(cat "$RAW_OUTPUT_FILE")"

# The local cdx router captures the provider stream in a sidecar log and
# prints that log path to stdout. Include the captured stream when classifying
# failures; otherwise provider quota errors are misreported as engine-down.
captured_log_path="$(cat "$SIDECAR_PATH_FILE" 2>/dev/null || true)"
if [[ -z "$captured_log_path" ]]; then
  captured_log_path="$(printf '%s\n' "$raw_output" | tail -n 1)"
fi
if [[ "$captured_log_path" == /* && -f "$captured_log_path" ]]; then
  raw_output+=$'\n'"$(cat "$captured_log_path")"
  cat "$captured_log_path" >> "$RAW_OUTPUT_FILE"
fi

echo "=== codex.sh exit=$RC ===" | tee -a "$LOG"
finalize_terminal "$RC" 'codex' "$THREAD_ID" codex "$MODEL"
