#!/usr/bin/env bash
# claude.sh — dispatch the Claude Code coder in ONE fail-closed command. WRAPPER-CONTRACT wrapper.
# audience: AI coding agents first. Invoke by path; do NOT re-derive the codepath.
#
# CONTRACT: claude.sh --workspace <dir> --trust "<prompt>" --task-slug <slug> --model <id>
#           [--timeout <secs>] [--resume <session-id>] | --health | --list-models
#   - Runs `claude -p` FOREGROUND under `timeout -k 5 <secs>`, stdin </dev/null.
#   - Raw engine json goes to the logfile; stdout carries ONLY the final status line.
#   - Exit: 0 completion, 75 rate-limited, 124 non-completion, 2 usage, 3 engine down.
#   - `--permission-mode bypassPermissions` needs IS_SANDBOX=1 when the caller is uid 0;
#     seat-entrypoint.sh sets it inside the seat container.
set -uo pipefail

WORKSPACE="" PROMPT="" TASK_SLUG="" MODEL="" RESUME_SESSION_ID="" PROFILE="" TIMEOUT=360
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;;
    --resume)    [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--resume requires a session id"}' >&2; exit 2; }; RESUME_SESSION_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;;
    --health) MODE="health"; shift;;
    --list-models) MODE="list-models"; shift;;
    *) shift;;
  esac
done
[[ -n "$PROFILE" ]] && echo "notice: --profile=$PROFILE ignored — claude.sh has no account routing" >&2

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
_CLAUDE_BIN="${_CLAUDE_ENGINE_BIN:-claude}"

claude_available() {
  if [[ "$_CLAUDE_BIN" == */* ]]; then
    [[ -x "$_CLAUDE_BIN" ]] || return 1
  else
    command -v "$_CLAUDE_BIN" >/dev/null 2>&1 || return 1
  fi
  return 0
}

case "$MODE" in
  health)
    claude_available || { echo '{"ok":false,"detail":"claude binary not found"}' >&2; exit 3; }
    timeout 10 "$_CLAUDE_BIN" auth status --json
    rc=$?
    case "$rc" in
      0) exit 0;;
      124|137) exit 124;;
      *) echo "{\"ok\":false,\"detail\":\"claude auth status failed rc=$rc\"}" >&2; exit 3;;
    esac
    ;;
  list-models)
    echo '{"ok":false,"detail":"claude.sh exposes no model inventory"}' >&2
    exit 2
    ;;
esac

TASK_SLUG="${TASK_SLUG//[^a-zA-Z0-9_-]/}"
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; }
claude_available || { echo '{"ok":false,"detail":"claude binary not found"}' >&2; exit 3; }

if [[ -f "$SKILL_DIR/wrappers/lib/remote-seat.sh" ]]; then
  source "$SKILL_DIR/wrappers/lib/remote-seat.sh"
  seat_remote_dispatch claude claude.sh "$WORKSPACE" "$PROMPT" "$TASK_SLUG" "$MODEL" "$TIMEOUT" "$PROFILE" --resume "$RESUME_SESSION_ID" || true
fi

DISPATCH_GUARD="$HOME/.claude/bin/local-dispatch-guard"
if [[ "${HARNESS_SEAT_CONTAINER:-}" == "1" ]]; then
  :
elif [[ -x "$DISPATCH_GUARD" ]]; then
  "$DISPATCH_GUARD" claude.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="${HARNESS_LOG_DIR:-$HOME/Projects/mega-plan-harness/tmp/logs}"
if ! mkdir -p "$LOG_DIR" 2>/dev/null || [[ ! -d "$LOG_DIR" || ! -w "$LOG_DIR" ]]; then
  LOG_DIR="${TMPDIR:-/tmp}/mega-plan-harness-logs"
  mkdir -p "$LOG_DIR" || { echo '{"ok":false,"detail":"no writable log dir"}' >&2; exit 3; }
fi
LOG="$LOG_DIR/$RUN_ID-$TASK_SLUG.log"

CLAUDE_RUN=(-p "$PROMPT" --model "$MODEL" --output-format json --permission-mode bypassPermissions)
[[ -n "$RESUME_SESSION_ID" ]] && CLAUDE_RUN+=(--resume "$RESUME_SESSION_ID")
CLAUDE_ARGS=(timeout -k 5 "$TIMEOUT" "$_CLAUDE_BIN" "${CLAUDE_RUN[@]}")
{
  echo "=== CMD: $(printf '%q ' "${CLAUDE_ARGS[@]}")"
  echo "=== PROMPT: $(printf '%q ' "$PROMPT")"
  echo "=== workspace: $WORKSPACE  timeout: ${TIMEOUT}s  log: $LOG ==="
  [[ -n "$RESUME_SESSION_ID" ]] && echo "=== resume: $RESUME_SESSION_ID ==="
} > "$LOG"

cd "$WORKSPACE" || { echo '{"ok":false,"detail":"cd workspace failed"}' >&2; exit 3; }
"${CLAUDE_ARGS[@]}" < /dev/null >> "$LOG" 2>&1
RC="$?"

readarray -t CLAUDE_PARSE < <(
  RAW_OUTPUT_FILE="$LOG" ENGINE_RC="$RC" RESUME_ID="$RESUME_SESSION_ID" python3 - "$LOG" <<'PY'
import json
import os
import re

path = os.environ["RAW_OUTPUT_FILE"]
resume_id = os.environ.get("RESUME_ID", "") or ""
engine_rc = int(os.environ.get("ENGINE_RC", "0") or 0)

raw_lines = []
try:
    with open(path, "r", encoding="utf-8") as handle:
        raw_lines = handle.read().splitlines()
except OSError:
    raw_lines = []

parsed = None
for line in raw_lines:
    line = line.strip()
    if not line.startswith("{") or not line.endswith("}"):
        continue
    try:
        candidate = json.loads(line)
    except Exception:
        continue
    if isinstance(candidate, dict):
        parsed = candidate


def to_text(value):
    if value is None:
        return ""
    if isinstance(value, str):
        return value
    if isinstance(value, (bool, int, float)):
        return str(value)
    try:
        return json.dumps(value, ensure_ascii=False)
    except Exception:
        return str(value)


def scan_resume_timestamp(lines):
    for line in lines:
        match = re.search(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})", line)
        if match:
            return match.group(0)
    return ""


# the wrapper's own preamble lines carry the prompt; only engine output may decide the rc
engine_lines = [line for line in raw_lines if not line.startswith("=== ")]
RATE_LIMIT_RE = re.compile(r"usage.?limit|rate.?limit|\b429\b", re.IGNORECASE)

status = {"ok": False, "detail": "unparseable or missing engine output"}
mapped_rc = 3

if engine_rc in (124, 137):
    mapped_rc = 124
    status = {"ok": False, "detail": "dispatch timed out or was killed"}
elif not parsed:
    if engine_rc == 0:
        mapped_rc = 3
        status = {"ok": False, "detail": "engine exited 0 with no parseable output"}
    elif RATE_LIMIT_RE.search("\n".join(engine_lines)):
        mapped_rc = 75
        status = {"ok": False, "detail": "rate-limited"}
    else:
        mapped_rc = engine_rc
        status = {"ok": False, "detail": f"engine exited {engine_rc} with no parseable output"}
else:
    is_error = bool(parsed.get("is_error"))
    result_text = to_text(parsed.get("result"))
    error_text = to_text(parsed.get("error"))
    session_id = to_text(parsed.get("session_id")) or resume_id
    api_error_status = to_text(parsed.get("api_error_status"))
    combined = "\n".join(value for value in (result_text, error_text, api_error_status) if value)
    is_rate_limited = bool(RATE_LIMIT_RE.search(combined))

    if engine_rc == 0 and not is_error:
        mapped_rc = 0
        status = {"ok": True, "detail": result_text.strip() or "completed", "session_id": session_id}
    elif is_rate_limited and (is_error or engine_rc != 0):
        mapped_rc = 75
        status = {"ok": False, "detail": "rate-limited", "session_id": session_id}
    elif engine_rc == 0:
        # the engine reported its own failure after a real dispatch — non-completion, never "nothing ran"
        mapped_rc = 124
        status = {"ok": False, "detail": result_text.strip() or error_text.strip() or "failed", "session_id": session_id}
    else:
        mapped_rc = engine_rc
        status = {"ok": False, "detail": result_text.strip() or error_text.strip() or "failed", "session_id": session_id}

if mapped_rc == 75:
    # the reset time only ever arrives as prose in the engine's own message
    resume_at = scan_resume_timestamp(engine_lines)
    if resume_at:
        status["resume_at"] = resume_at

print(mapped_rc)
print(json.dumps(status, separators=(",", ":")))
PY
)

if [[ "${#CLAUDE_PARSE[@]}" -ne 2 ]]; then
  echo '{"ok":false,"detail":"status parse failed"}' >&2
  exit 3
fi

CLAUDE_RC="${CLAUDE_PARSE[0]}"
CLAUDE_STATUS="${CLAUDE_PARSE[1]}"
printf '%s\n' "$CLAUDE_STATUS"
exit "$CLAUDE_RC"
