#!/usr/bin/env bash
# factory-dispatch.sh — fail-closed dispatch ledger for the factory-adoption build run.
# audience: AI coding agents first. Invoke BY PATH; never call ca.sh raw during this run.
#
# CONTRACT: factory-dispatch.sh --task <slug> --workspace <dir> --trust "<prompt>" --model <id> [--timeout <secs>]
#   Wraps modules/harness/wrappers/ca.sh. Appends every outcome to <workspace>/.factory-ledger.jsonl.
#   REFUSES (before dispatching) with distinct exit codes:
#     10 = task budget exhausted (>= MAX_PER_TASK prior dispatches for --task)
#     11 = identical failure signature already seen twice for --task (rc + first stderr line)
#     12 = global cap reached (>= MAX_GLOBAL dispatches in ledger)
#   Otherwise passes through ca.sh's exit code (0/124/2/3).
#   Caps are NOT settable from the dispatching agent's environment. Raising one requires a
#   coordinator-written one-shot grant at <workspace>/.factory-grant.json:
#     {"task":"<slug>","max_per_task":4,"reason":"why"}   (max_global optional)
#   The grant applies only to its own --task, is deleted on use, and is recorded in the ledger.
#   FD_CA_SH re-points the wrapped binary (test seam); it cannot weaken a cap.
#   Placement: on the laptop this call forwards whole to a buildbox via run-remote and
#   waits; on a registry host, or once already placed (RUN_REMOTE_PLACED=1), it dispatches
#   here. FACTORY_PLACEMENT=local is the kill switch; no reachable buildbox fails closed,
#   never falls back to running the model here.

set -uo pipefail

MAX_PER_TASK=2
MAX_GLOBAL=30
CA_SH="${FD_CA_SH:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/wrappers/ca.sh}"

if [[ "${RUN_REMOTE_PLACED:-}" != "1" && "${FACTORY_PLACEMENT:-}" != "local" ]]; then
  fd_script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  fd_registry="${fd_script_dir}/../../workstation/claude/lib/buildbox-registry.mjs"
  if [[ ! -f "$fd_registry" ]]; then
    echo '{"ok":false,"detail":"buildbox registry missing — set FACTORY_PLACEMENT=local to run here"}' >&2
    exit 2
  fi
  fd_self_code=0
  node "$fd_registry" self >/dev/null 2>&1 || fd_self_code=$?
  if [[ "$fd_self_code" -ne 0 && "$fd_self_code" -ne 4 ]]; then
    echo '{"ok":false,"detail":"buildbox registry unreadable or invalid — set FACTORY_PLACEMENT=local to run here"}' >&2
    exit 2
  fi
  if [[ "$fd_self_code" -eq 4 ]]; then
    if ! fd_hosts="$(node "$fd_registry" hosts --order build 2>/dev/null)" || [[ -z "$fd_hosts" ]]; then
      echo '{"ok":false,"detail":"no reachable buildbox to place this dispatch — set FACTORY_PLACEMENT=local to run here"}' >&2
      exit 2
    fi
    fd_quoted=""
    for fd_arg in "$@"; do
      fd_quoted+=" $(printf '%q' "$fd_arg")"
    done
    exec run-remote launch --wait -- "modules/harness/tools/factory-dispatch.sh${fd_quoted}"
  fi
fi

TASK="" WORKSPACE="" ARGS=()
while [[ $# -gt 0 ]]; do
  case "$1" in
    --task) TASK="${2:-}"; shift 2;;
    --workspace) WORKSPACE="${2:-}"; ARGS+=("$1" "${2:-}"); shift 2;;
    *) ARGS+=("$1"); [[ $# -gt 1 && "$1" == --* && "${2:-}" != --* ]] && { ARGS+=("${2:-}"); shift; }; shift;;
  esac
done

[[ -z "$TASK" ]] && { echo '{"ok":false,"detail":"--task required"}' >&2; exit 2; }
[[ -z "$WORKSPACE" || ! -d "$WORKSPACE" ]] && { echo '{"ok":false,"detail":"--workspace missing or not a dir"}' >&2; exit 2; }
[[ -x "$CA_SH" ]] || { echo "{\"ok\":false,\"detail\":\"ca.sh not executable: $CA_SH\"}" >&2; exit 2; }

LEDGER="$WORKSPACE/.factory-ledger.jsonl"
touch "$LEDGER" || { echo '{"ok":false,"detail":"ledger not writable"}' >&2; exit 2; }

GRANT="$WORKSPACE/.factory-grant.json"
if [[ -f "$GRANT" ]]; then
  grant_line="$(python3 - "$GRANT" "$TASK" <<'PY'
import json, sys
try:
    g = json.load(open(sys.argv[1]))
except Exception as exc:
    print(f"ERR unreadable grant: {exc}"); raise SystemExit(0)
if not isinstance(g, dict):
    print("ERR grant is not an object"); raise SystemExit(0)
if g.get("task") != sys.argv[2]:
    print("SKIP"); raise SystemExit(0)
reason = str(g.get("reason", "")).strip()
if not reason:
    print("ERR grant missing reason"); raise SystemExit(0)
per = g.get("max_per_task"); glob = g.get("max_global")
for value in (per, glob):
    if value is not None and not (isinstance(value, int) and value > 0):
        print("ERR grant cap must be a positive integer"); raise SystemExit(0)
if per is None and glob is None:
    print("ERR grant raises no cap"); raise SystemExit(0)
print("OK", per if per is not None else "-", glob if glob is not None else "-", reason.replace('"', "'"))
PY
)"
  case "$grant_line" in
    ERR*) echo "{\"ok\":false,\"detail\":\"${grant_line#ERR }\"}" >&2; exit 2;;
    OK*)
      read -r _ grant_per grant_global grant_reason <<<"$grant_line"
      [[ "$grant_per" != "-" ]] && MAX_PER_TASK="$grant_per"
      [[ "$grant_global" != "-" ]] && MAX_GLOBAL="$grant_global"
      rm -f "$GRANT"
      printf '{"kind":"override","task":"%s","ts":"%s","max_per_task":%s,"max_global":%s,"reason":"%s"}\n' \
        "$TASK" "$(date -Is)" "${grant_per/-/null}" "${grant_global/-/null}" "$grant_reason" >> "$LEDGER"
      ;;
  esac
fi

global_count=$(grep -c '"kind":"dispatch"' "$LEDGER" 2>/dev/null); global_count=${global_count:-0}
task_count=$(grep -c "\"kind\":\"dispatch\",\"task\":\"$TASK\"" "$LEDGER" 2>/dev/null); task_count=${task_count:-0}

if (( global_count >= MAX_GLOBAL )); then
  echo "{\"ok\":false,\"refused\":\"global-cap\",\"count\":$global_count}" >&2; exit 12
fi
if (( task_count >= MAX_PER_TASK )); then
  echo "{\"ok\":false,\"refused\":\"task-budget\",\"task\":\"$TASK\",\"count\":$task_count}" >&2; exit 10
fi

STDERR_FILE="$(mktemp)"
trap 'rm -f "$STDERR_FILE"' EXIT
"$CA_SH" --task-slug "$TASK" "${ARGS[@]}" 2> >(tee "$STDERR_FILE" >&2)
rc=$?

first_err="$(head -1 "$STDERR_FILE" | tr -d '"\\' | cut -c1-160)"
sig="${rc}|${first_err}"
ts="$(date -Is)"
printf '{"kind":"dispatch","task":"%s","ts":"%s","rc":%d,"sig":"%s"}\n' "$TASK" "$ts" "$rc" "$sig" >> "$LEDGER"

if (( rc != 0 )); then
  sig_count=$(grep -F "\"sig\":\"$sig\"" "$LEDGER" | grep -c "\"task\":\"$TASK\"")
  if (( sig_count >= 2 )); then
    echo "{\"ok\":false,\"refused\":\"repeat-failure\",\"task\":\"$TASK\",\"sig_count\":$sig_count}" >&2
    exit 11
  fi
fi
exit "$rc"
