#!/usr/bin/env bash
# ccr-up.sh — ensure the ccr proxy (north's OpenRouter gateway) is listening, WITHOUT BLOCKING.
# audience: AI coding agents first. This is the EXTRACTED, TESTED form of run-plan implementNorth step 1
# and north-orchestrator's "bring ccr up" prose. An agent must invoke it by path and read the JSON —
# it must NOT re-derive or reformulate the launch.
#
# WHY this is a script and not prose: `ccr start` is a LONG-LIVED DAEMON. An LLM that re-derives the
# launch has twice turned a detached background start into a blocking foreground pipe
# (`ccr start | tail`) and hung an entire run ~46 min. The one correct, non-blocking launch lives here,
# once, behind a test that proves it cannot hang.
#
# CONTRACT (stable surface): ccr-up.sh <slug>
#   <slug> — only used to name the daemon log ($CCR_TMP_ROOT/ccr-<slug>.log, default ~/tmp). No other positional args, so
#            north-orchestrator can reuse this unchanged.
#   Prints ONE line of JSON: {"up":bool,"detail":"..."}.
#   Exit 0 on ANY completed check (up OR not-up — consumer reads `up`, never the exit code).
#   Non-zero ONLY on usage fault (missing slug).
#
# TEST-ONLY SEAMS (env; production defaults reproduce the original prose exactly + flock):
#   CCR_PORT       (3456)      port to probe / lock-scope. Production never overrides.
#   CCR_CMD        ("ccr start") launch command. Tests inject a fake daemon.
#   CCR_UP_TIMEOUT (20)        readiness-poll seconds. Tests shorten to keep the suite fast.
#   CCR_LOCK_WAIT  (timeout+5) max seconds to wait for the single-flight lock.
#
# SINGLE-FLIGHT (flock): run-plan runs tasks concurrently, so N north dispatches can call this at the
# same instant. A bare probe-then-launch is TOCTOU — all probe down, all fire `ccr start`. We do NOT
# rely on ccr's (unverified, unguessed) behavior when 3456 is already bound; flock makes the launch
# single-flight unconditionally. Fast-path probe is OUTSIDE the lock (common case = already up, no
# contention); when down we take the lock and RE-PROBE (double-checked) before launching, so a caller
# that waited on the lock while a sibling launched just sees `up` and returns without a second launch.
# CRITICAL: the launched daemon must NOT inherit the lock fd (9>&- closes it in the child) — otherwise
# the daemon holds the lock for its whole life and the next caller deadlocks. This is regression-tested.
set -uo pipefail

# Global singleton daemon (keyed by port, not by repo) — no repoRoot to anchor under, so this is the
# one harness path that lives directly in ~/tmp rather than <repo>/tmp. Still NEVER /tmp.
CCR_TMP_ROOT="${CCR_TMP_ROOT:-$HOME/tmp}"; mkdir -p "$CCR_TMP_ROOT"

PORT="${CCR_PORT:-3456}"
CCR_CMD="${CCR_CMD:-ccr start}"
TIMEOUT="${CCR_UP_TIMEOUT:-20}"
LOCK_WAIT="${CCR_LOCK_WAIT:-$((TIMEOUT + 5))}"

# JSON string escaper (backslash, quote, tab/newline -> space). Self-contained so this file stays
# reusable in isolation (north-orchestrator) — a 6-line escaper is not worth coupling two libs.
jstr() {
  local s=$1
  s=${s//\\/\\\\}
  s=${s//\"/\\\"}
  s=${s//$'\t'/ }
  s=${s//$'\n'/ }
  printf '%s' "$s"
}

emit_up() { printf '{"up":%s,"detail":"%s"}\n' "$1" "$(jstr "$2")"; }

# probe: 0 if something is listening and answering on the proxy port, non-zero otherwise.
# -f = readiness requires an HTTP 2xx, not just an open socket. VERIFIED against real ccr
# (2026-06-27): `ccr start` returns HTTP 200 on / once up, so -f succeeds. If a future ccr
# version stops returning 2xx on / (e.g. 404 with no route there), -f would falsely report
# down forever — then drop -f (bare connection = ready) and re-run the suite (fakes return 200,
# tests stay green either way). Do NOT change this on a hunch; it is measured.
probe() { curl -sf "127.0.0.1:${PORT}" >/dev/null 2>&1; }

ccr_up() {
  local slug=${1:-}
  [[ -n "$slug" ]] || { echo '{"up":false,"detail":"usage: ccr-up.sh <slug>"}'; return 2; }

  # Fast path — already up. No lock, no launch (idempotent).
  if probe; then emit_up true "ccr proxy already up on ${PORT}"; return 0; fi

  # Down: serialize the launch decision. fd 9 = the single-flight lock.
  local lockfile="$CCR_TMP_ROOT/ccr-${PORT}.lock"
  exec 9>"$lockfile" 2>/dev/null || { emit_up false "cannot open ccr lock file ${lockfile}"; return 0; }
  if ! flock -w "$LOCK_WAIT" 9; then
    emit_up false "ccr launch lock contended (waited ${LOCK_WAIT}s)"; return 0
  fi

  # Double-checked: a sibling may have brought it up while we waited for the lock.
  if probe; then emit_up true "ccr proxy up on ${PORT} (launched by sibling)"; return 0; fi

  # Verify the launch binary exists BEFORE trying — distinct, debuggable detail vs a generic timeout.
  local binname=${CCR_CMD%% *}
  if ! command -v "$binname" >/dev/null 2>&1; then
    emit_up false "ccr binary not found: ${binname}"; return 0
  fi

  # The ONE correct non-blocking launch: new session, stdout/stderr -> log, stdin from /dev/null,
  # lock fd 9 CLOSED in the child (9>&-) so the daemon never holds the lock, backgrounded + disowned.
  # NEVER pipe ccr's stdout, NEVER foreground it. $CCR_CMD is intentionally word-split.
  setsid $CCR_CMD >"$CCR_TMP_ROOT/ccr-${slug}.log" 2>&1 </dev/null 9>&- & disown

  # Bounded readiness poll — this loop CANNOT exceed TIMEOUT seconds.
  local i
  for ((i = 0; i < TIMEOUT; i++)); do
    probe && { emit_up true "ccr proxy came up on ${PORT} after ~${i}s"; return 0; }
    sleep 1
  done
  emit_up false "ccr proxy unavailable on ${PORT} after ${TIMEOUT}s (see $CCR_TMP_ROOT/ccr-${slug}.log)"
  return 0
}

# Source-guard: tests source this file to unit-test probe/emit_up/jstr; direct run dispatches.
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  ccr_up "${1:-}"
fi
