#!/usr/bin/env bash
# ca.sh — dispatch the cursor/composer coder in ONE fail-closed command. cursor's north twin.
# audience: AI coding agents first. Invoke by path; do NOT re-derive the env block or the codepath.
#
# WHY this exists: this CONTRACT wrapper centralizes the prompt (no re-derivation), pins model, scopes env,
# bounds with timeout -k 5, and emits the WRAPPER-CONTRACT exit codes (0/124/2/3). The original na.sh skill
# was inline prose (the re-derivation hazard). This wrapper kills the hazard AND presents the fixed contract
# flag set to the orchestrator.
#
# CONTRACT: ca.sh --workspace <dir> --trust "<prompt>" --task-slug <slug> [--model <id>] [--timeout <secs>]
#   - cd <workspace> (SHORT path — long absolute CWD triggers phantom-tree write bug).
#   - Runs cursor-agent FOREGROUND, bounded by `timeout -k 5 <secs>` (default 360); stdin </dev/null (cannot hang).
#   - Logs raw json to ~/Projects/mega-plan-harness/tmp/logs/<datetime>-<slug>.log (same dir as na.sh).
#   - Exit: 0 = completion (engine ran to completion), 124 = timeout/killed (retry/backoff), 2 = usage error
#     (bad/missing args), 3 = engine down (precondition failed, NO dispatch) OR agent returned 0 but produced
#     empty captured output (silent write failure / disk full — success requires evidence). NO grok fallback —
#     the fallback is the orchestrator's concern (WRAPPER-CONTRACT).

set -uo pipefail

WORKSPACE="" PROMPT="" TASK_SLUG="" MODEL="" RESUME_CHAT_ID="" PROFILE="" TIMEOUT=360
HEALTH_MODE=0
LIST_MODELS_MODE=0
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 chat id"}' >&2; exit 2; }; RESUME_CHAT_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)    HEALTH_MODE=1; shift;;
    --list-models) LIST_MODELS_MODE=1; shift;;
    *) shift;;
  esac
done
[[ -n "$PROFILE" ]] && echo "notice: --profile=$PROFILE ignored — ca.sh has no account routing" >&2

SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/.."
source "$SKILL_DIR/wrappers/lib/finalize.sh"
source "$SKILL_DIR/wrappers/lib/spend-cap.sh"
# CURSOR.env is an optional env overlay (auth lives in ${XDG_CONFIG_HOME:-~/.config}/cursor/auth.json;
# ~/.cursor holds configuration only); absence is fine, a failed source is fatal.
load_cursor_env() {
  local env_candidates=(
    "${_CURSOR_ENV:-}"
    "$SKILL_DIR/CURSOR.env"
    "$HOME/.claude/CURSOR.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
}

if [[ $HEALTH_MODE -eq 1 ]]; then
  load_cursor_env || { echo '{"ok":false,"detail":"CURSOR.env source failed — engine precondition unmet"}' >&2; exit 3; }
  cursor-agent status --format json
  exit $?
fi

if [[ $LIST_MODELS_MODE -eq 1 ]]; then
  load_cursor_env || { echo '{"ok":false,"detail":"CURSOR.env source failed — engine precondition unmet"}' >&2; exit 3; }
  cursor-agent models
  exit $?
fi

# Sanitize TASK_SLUG — strip path-traversal chars; only [a-zA-Z0-9_-] allowed
TASK_SLUG="${TASK_SLUG//[^a-zA-Z0-9_-]/}"
RUN_ID="$(date +%Y-%m-%d-%H-%M-%S)-$$"

# Validate required flags
if [[ -z "$WORKSPACE" || ! -d "$WORKSPACE" ]]; then
  echo '{"ok":false,"detail":"--workspace missing or not a dir"}' >&2
  exit 2
fi
if [[ -z "$PROMPT" ]]; then
  echo '{"ok":false,"detail":"--trust (prompt) required"}' >&2
  exit 2
fi
if [[ -z "$TASK_SLUG" ]]; then
  echo '{"ok":false,"detail":"--task-slug required (log + retry hygiene)"}' >&2
  exit 2
fi
if [[ -z "$MODEL" ]]; then
  echo '{"ok":false,"detail":"--model required (no silent default)"}' >&2
  exit 2
fi
# `-fast` cursor variants are refused here, at the one choke point every cursor
# dispatch passes through — a hook cannot see a model id chosen inside a script.
# Two spellings both select the fast tier: a `-fast` suffix id, and the
# parameterized override form `id[...,fast=true,...]`. Both are checked
# case-insensitively.
MODEL_LC="${MODEL,,}"
FAST_HIT=0 FAST_SUGGEST=""
if [[ "$MODEL_LC" == *-fast ]]; then
  FAST_HIT=1
  FAST_SUGGEST="${MODEL::-5}"
elif [[ "$MODEL" =~ ^([^][]+)\[(.*)\]$ ]]; then
  BASE_ID="${BASH_REMATCH[1]}"
  IFS=',' read -ra _FAST_PARAMS <<< "${BASH_REMATCH[2]}"
  for _param in "${_FAST_PARAMS[@]}"; do
    _param="${_param// /}"
    if [[ "${_param,,}" == "fast=true" ]]; then
      FAST_HIT=1
      FAST_SUGGEST="$BASE_ID"
      break
    fi
  done
fi
if [[ $FAST_HIT -eq 1 ]]; then
  if [[ -n "$FAST_SUGGEST" ]]; then
    echo "{\"ok\":false,\"detail\":\"--model '$MODEL' selects the -fast tier and is REFUSED. Use '$FAST_SUGGEST'. No override flag exists; urgency is not a reason.\"}" >&2
  else
    echo "{\"ok\":false,\"detail\":\"--model '$MODEL' selects the -fast tier and is REFUSED. No override flag exists; urgency is not a reason.\"}" >&2
  fi
  exit 2
fi

spend_cap_refuse_if_capped roy-grok 'cursor-agent' "$RESUME_CHAT_ID" '' "$MODEL"
load_cursor_env || { echo '{"ok":false,"detail":"CURSOR.env source failed — engine precondition unmet"}' >&2; exit 3; }

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

# Reaching here means the seat was NOT remoted and cursor-agent is about to run here. Inside a seat container that is the
# sanctioned execution plane, so the guard is skipped. Headless callers exit 97 naming the remote path; a terminal on any std fd means
# the owner is driving and the dispatch proceeds. A missing guard refuses the same headless
# dispatches it would have refused.
DISPATCH_GUARD="$HOME/.claude/bin/local-dispatch-guard"
if [[ "${HARNESS_SEAT_CONTAINER:-}" == "1" ]]; then
  :
elif [[ -x "$DISPATCH_GUARD" ]]; then
  "$DISPATCH_GUARD" ca.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

# Wrapper identity contract (fail-open):
# - After seat_remote_dispatch returns (local / in-container path that launches cursor-agent).
# - Probe: timeout -k 5 10 cursor-agent status --format json </dev/null; single field userInfo.email.
# - When HARNESS_IDENTITY_FILE is set and non-empty, atomically write one JSON object
#   {"kind":"wrapper.identity","account":"<email>"} to that path (.tmp sibling then mv).
# - Emit NOTHING on stderr/stdout for identity. Unset/empty sink => no-op.
# - Never print status blobs or credential/token material.
emit_wrapper_identity() {
  [[ -n "${HARNESS_IDENTITY_FILE:-}" ]] || return 0
  local line=""
  line="$(
    timeout -k 5 10 cursor-agent status --format json </dev/null 2>/dev/null \
      | python3 -c '
import json, sys
try:
    parsed = json.loads(sys.stdin.read())
    email = (parsed.get("userInfo") or {}).get("email")
    if not isinstance(email, str):
        raise SystemExit(1)
    account = email.strip()
    if not account or len(account) > 320 or any(ord(ch) < 32 or ord(ch) == 127 for ch in account):
        raise SystemExit(1)
    sys.stdout.write(json.dumps({"kind": "wrapper.identity", "account": account}, separators=(",", ":")))
except Exception:
    raise SystemExit(1)
' 2>/dev/null
  )" || return 0
  [[ -n "$line" ]] || return 0
  local tmp="${HARNESS_IDENTITY_FILE}.tmp"
  printf '%s\n' "$line" >"$tmp" || { rm -f "$tmp"; return 0; }
  mv -f "$tmp" "$HARNESS_IDENTITY_FILE" || { rm -f "$tmp"; return 0; }
}
emit_wrapper_identity || true

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":"log dir create failed"}' >&2; exit 3; }
fi
LOG="$LOG_DIR/$RUN_ID-$TASK_SLUG.log"
RAW_OUTPUT_FILE="$LOG_DIR/$RUN_ID-$TASK_SLUG.raw"
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="${CA_TERMINAL_GRACE_SECS:-2}"
TRANSCRIPT_PATH="${HARNESS_TRANSCRIPT_PATH:-}"
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
}

CHAT_ID="$RESUME_CHAT_ID"
if [[ -z "$CHAT_ID" ]]; then
  # create-chat can keep running after it has printed the id (it indexes the workspace first);
  # an unbounded command substitution then hangs the whole dispatch, which the contract forbids.
  # The id on stdout is the result — take it and move on.
  if CHAT_ID="$(timeout -k 5 "${CA_CREATE_CHAT_TIMEOUT:-120}" cursor-agent create-chat 2>>"$LOG")"; then
    :
  else
    create_chat_rc=$?
    if [[ -n "$(printf '%s\n' "$CHAT_ID" | tail -n 1 | tr -d '\r')" ]]; then
      create_chat_rc=0
    fi
  fi
  if [[ ${create_chat_rc:-0} -ne 0 ]]; then
    if [[ $create_chat_rc -eq 124 ]] || [[ $create_chat_rc -eq 137 ]]; then
      exit 124
    elif [[ $create_chat_rc -eq 2 ]]; then
      exit 2
    else
      echo '{"ok":false,"detail":"cursor-agent create-chat failed"}' >&2
      exit 3
    fi
  fi
  CHAT_ID="$(printf '%s\n' "$CHAT_ID" | tail -n 1 | tr -d '\r')"
fi
if [[ -z "$CHAT_ID" ]]; then
  CHAT_ID="chat-$RUN_ID"
fi

# Dry-run command: same source-array used for log header and run line -> no divergence.
CURSOR_ARGS=(timeout -k 5 "$TIMEOUT" cursor-agent --model "$MODEL" --print --output-format stream-json --stream-partial-output --workspace "$WORKSPACE" --trust --resume "$CHAT_ID" "$PROMPT")
CURSOR_CMD=$(printf '%q ' "${CURSOR_ARGS[@]}")
{
  echo "=== TOOL: cursor-agent (${MODEL:-composer-2.5}) ==="
  echo "=== CMD: $CURSOR_CMD ==="
  echo "=== workspace: $WORKSPACE  timeout: ${TIMEOUT}s  log: $LOG ==="
} | tee "$LOG"

# cursor-agent keeps running after it prints its terminal result event. The wrapper
# ends it here: the orchestrator's own terminal-grace kill signals the whole process
# group, which takes ca.sh down before it 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":"result"' "$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
}

# Execute in the workspace, foreground, stdin closed, captured stderr to log
cd "$WORKSPACE" || { echo '{"ok":false,"detail":"cd workspace failed"}' >&2; exit 2; }
(
  "${CURSOR_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_output
RC="$(cat "$CHILD_RC_FILE" 2>/dev/null || printf '3')"
if [[ -f "$TERMINAL_REAPED_FILE" ]]; then RC=0; fi
rm -f "$CHILD_RC_FILE" "$TERMINAL_REAPED_FILE"

echo "=== ca.sh exit=$RC ===" | tee -a "$LOG"
finalize_terminal "$RC" 'cursor-agent' "$CHAT_ID" '' "$MODEL"
