#!/usr/bin/env bash
# cx-implement.sh — codex (gpt-5.5) IMPLEMENT leg in one fail-closed command. run-plan's
# code-WRITING twin of ca.sh (cursor) and the implement-side complement to cx.sh (review/commit).
# audience: AI coding/orchestration agents first. Invoke BY PATH; do NOT re-derive the codex exec line.
#
# WHY this exists: naive `codex exec '<prompt>'` is wrong for unattended implementation — it reads the
# prompt from stdin if the arg is absent (hangs), runs read-only by default (writes nothing), prompts
# for approval (hangs), and picks codex's config-default model/effort (not gpt-5.5). This wrapper pins
# workspace-write + approval_policy=never + gpt-5.5 + effort, closes stdin, captures the full log, then
# COMMITS the draft itself (codex may or may not commit) so the caller gets a deterministic head sha.
#
# CONTRACT:
#   cx-implement.sh --worktree DIR --task-slug SLUG --prompt-file FILE [--effort high|xhigh|medium|low] [--timeout SECS]
#   - Pins: codex exec --skip-git-repo-check -C DIR -m gpt-5.5 -s workspace-write \
#           -c approval_policy=never -c model_reasoning_effort=<effort> -o <last> --color never "<prompt>" </dev/null
#   - Prompt is read from FILE (the task contract) and passed as the PROMPT arg; stdin is closed.
#   - After codex returns, stages everything and commits a draft IF the tree changed and HEAD did not move
#     (codex didn't self-commit). Idempotent: a clean tree with HEAD advanced is treated as codex-self-committed.
#   - STDOUT: exactly one JSON line —
#       success:  {"ok":true,"baseSha":"<sha>","headSha":"<sha>","files":["a","b"],"commitCount":N,"detail":"<short>"}
#       failure:  {"ok":false,"baseSha":"<sha>","headSha":"<sha>","files":[],"commitCount":0,"detail":"<short>"}
#     ok=false is a VERDICT (the caller falls back / halts), never a crash. exit 0 on any codex outcome.
#   - exit 2 ONLY on caller (usage) error — bad/missing args.
#   - BOUNDED RETRY: rc=124 (timeout) or a detected usage/rate limit → retry up to CXI_RETRIES (default 2).
#   - TEST SEAM: CXI_CODEX_BIN overrides the `codex` binary (test-cx-implement.sh injects a mock). Default: codex.
set -uo pipefail

emit() { printf '%s\n' "$1"; exit 0; }
jstr() { printf '%s' "${1:-}" | tr -d '\n' | sed 's/\\/\\\\/g; s/"/\\"/g' | cut -c1-300; }
usage() { printf '{"ok":false,"baseSha":"","headSha":"","files":[],"commitCount":0,"detail":"usage: %s"}\n' "$(jstr "$*")" >&2; exit 2; }

WORKTREE=""; TASK_SLUG=""; PROMPT_FILE=""; EFFORT="high"; TIMEOUT="${CXI_TIMEOUT:-3480}"; ACCOUNT="${CXI_ACCOUNT:-}"
while [[ $# -gt 0 ]]; do
  case "$1" in
    --worktree)    WORKTREE="${2:-}";    shift 2;;
    --task-slug)   TASK_SLUG="${2:-}";   shift 2;;
    --prompt-file) PROMPT_FILE="${2:-}"; shift 2;;
    --effort)      EFFORT="${2:-}";      shift 2;;
    --timeout)     TIMEOUT="${2:-}";     shift 2;;
    --model)       MODEL="${2:-}";       shift 2;;
    --account)     ACCOUNT="${2:-}";     shift 2;;
    *) usage "unknown arg: $1";;
  esac
done
[[ -n "$WORKTREE" && -d "$WORKTREE" ]] || usage "--worktree missing or not a dir"
[[ -n "$TASK_SLUG" ]] || usage "--task-slug required"
[[ -n "$PROMPT_FILE" && -f "$PROMPT_FILE" ]] || usage "--prompt-file missing or not a file"
case "$EFFORT" in high|xhigh|medium|low) : ;; *) usage "--effort must be high|xhigh|medium|low" ;; esac
case "$TIMEOUT" in ''|*[!0-9]*) usage "--timeout must be integer seconds" ;; esac
CODEX_BIN="${CXI_CODEX_BIN:-codex}"
MODEL="${MODEL:-gpt-5.4}"
PROMPT="$(cat "$PROMPT_FILE")"
[[ -n "$PROMPT" ]] || usage "--prompt-file is empty"

if [[ "$WORKTREE" == /home/user/Projects/multideal/tmp/wt-* ]]; then
  export NODE_AUTH_TOKEN="${NODE_AUTH_TOKEN:-dummy}"
  if [[ ! -e "$WORKTREE/apps/web/eslint.config.js" && -e /home/user/Projects/multideal/apps/web/eslint.config.js ]]; then
    ln -sfn /home/user/Projects/multideal/apps/web/eslint.config.js "$WORKTREE/apps/web/eslint.config.js"
  fi
  if [[ ! -e "$WORKTREE/docs" && -e /home/user/Projects/multideal/docs ]]; then
    ln -sfn /home/user/Projects/multideal/docs "$WORKTREE/docs"
    git -C "$WORKTREE" rev-parse --git-path info/exclude >/tmp/cxi-exclude-path.$$ 2>/dev/null || true
    if [[ -s /tmp/cxi-exclude-path.$$ ]]; then
      EXCLUDE_PATH="$(cat /tmp/cxi-exclude-path.$$)"
      grep -qx 'docs' "$EXCLUDE_PATH" 2>/dev/null || printf '\ndocs\n' >>"$EXCLUDE_PATH"
    fi
    rm -f /tmp/cxi-exclude-path.$$
  fi
fi

# Account selection. --account/CXI_ACCOUNT set → route the codex launch through `cdx --profile <slug>`
# (cdx resolves that account's CODEX_HOME per-invocation; NEVER repoints the shared ~/.codex/auth.json
# symlink). Unset → legacy behavior: derive CODEX_HOME from the current symlink target.
LAUNCH=()
if [[ -n "$ACCOUNT" ]]; then
  command -v cdx >/dev/null 2>&1 || usage "--account $ACCOUNT set but 'cdx' not on PATH"
  [[ -d "${HOME}/.systray-ai/accounts/${ACCOUNT}/CODEX_HOME" ]] || usage "--account: unknown account '$ACCOUNT' (no ~/.systray-ai/accounts/$ACCOUNT/CODEX_HOME)"
  LAUNCH=(cdx --profile "$ACCOUNT")
else
  DEFAULT_AUTH="${HOME}/.codex/auth.json"
  if [[ -e "$DEFAULT_AUTH" ]]; then
    RESOLVED_AUTH="$(readlink -f "$DEFAULT_AUTH" 2>/dev/null || true)"
    if [[ -n "$RESOLVED_AUTH" ]]; then
      RESOLVED_HOME="$(dirname "$RESOLVED_AUTH")"
      if [[ -d "$RESOLVED_HOME" ]]; then
        LAUNCH=(env "CODEX_HOME=$RESOLVED_HOME" "$CODEX_BIN")
      fi
    fi
  fi
  [[ ${#LAUNCH[@]} -gt 0 ]] || LAUNCH=("$CODEX_BIN")
fi

git_q() { git -C "$WORKTREE" "$@" 2>/dev/null; }
BASE_SHA="$(git_q rev-parse HEAD || echo "")"
[[ -n "$BASE_SHA" ]] || emit "{\"ok\":false,\"baseSha\":\"\",\"headSha\":\"\",\"files\":[],\"commitCount\":0,\"detail\":\"worktree has no HEAD commit\"}"

# Logs MUST live outside the worktree, else `git add -A` commits them and a no-op dispatch
# looks like a change. Default to a temp dir keyed by worktree basename; caller may override.
LOGDIR="${CXI_LOGDIR:-$(dirname "$WORKTREE")/cx-implement-logs/$(basename "$WORKTREE")}"; mkdir -p "$LOGDIR"   # never /tmp — dirname($WORKTREE) IS the resolved tmp root
LOG="$LOGDIR/${TASK_SLUG}.log"; LAST="$LOGDIR/${TASK_SLUG}.last"; ERR="$LOGDIR/${TASK_SLUG}.err"
: > "$LOG"

RETRIES="${CXI_RETRIES:-2}"; case "$RETRIES" in ''|*[!0-9]*) RETRIES=2;; esac; [[ "$RETRIES" -lt 1 ]] && RETRIES=1
RSLEEP="${CXI_RETRY_SLEEP:-6}"; case "$RSLEEP" in ''|*[!0-9]*) RSLEEP=6;; esac

RC=1; DETAIL=""
attempt=0
while [[ $attempt -lt $RETRIES ]]; do
  attempt=$((attempt+1))
  EXTRA_ARGS=()
  if [[ "$MODEL" == gpt-5.3-codex-spark* ]]; then
    EXTRA_ARGS+=(-c 'plugins."canva@openai-curated".enabled=false')
  fi
  {
    echo "=== TOOL: codex exec (implement) attempt=$attempt effort=$EFFORT timeout=${TIMEOUT}s account=${ACCOUNT:-<symlink>} ==="
    printf 'cmd: '
    printf '%q ' "${LAUNCH[@]}"
    printf 'exec '
    if [[ ${#EXTRA_ARGS[@]} -gt 0 ]]; then
      printf '%q ' "${EXTRA_ARGS[@]}"
    fi
    printf -- '--skip-git-repo-check -C %q -m %q -s workspace-write -c approval_policy=never -c model_reasoning_effort=%q -o %q --color never <PROMPT>\n' "$WORKTREE" "$MODEL" "$EFFORT" "$LAST"
  } >>"$LOG"
  timeout "$TIMEOUT" "${LAUNCH[@]}" exec "${EXTRA_ARGS[@]}" --skip-git-repo-check -C "$WORKTREE" \
    -m "$MODEL" -s workspace-write -c approval_policy=never -c model_reasoning_effort="$EFFORT" \
    -o "$LAST" --color never "$PROMPT" </dev/null >>"$LOG" 2>"$ERR"
  RC=$?
  cat "$ERR" >>"$LOG"
  if [[ $RC -eq 0 ]]; then DETAIL="codex exec ok"; break; fi
  if [[ $RC -eq 124 ]]; then
    DETAIL="codex timeout (rc=124) attempt $attempt"; echo "$DETAIL" >>"$LOG"
    [[ $attempt -lt $RETRIES ]] && { sleep "$RSLEEP"; continue; } || break
  fi
  # usage/rate limit → retry; any other rc → stop (real failure, fall back)
  if grep -qiE 'rate limit|429|usage limit|quota|temporarily unavailable|overloaded|at capacity' "$ERR"; then
    DETAIL="codex transient (rc=$RC) attempt $attempt"; echo "$DETAIL" >>"$LOG"
    [[ $attempt -lt $RETRIES ]] && { sleep "$RSLEEP"; continue; } || break
  fi
  DETAIL="codex rc=$RC: $(tail -1 "$ERR" 2>/dev/null)"; break
done

# Reconcile tree state → deterministic commit. codex may have: (a) self-committed, (b) left a dirty tree,
# (c) done nothing.
HEAD_NOW="$(git_q rev-parse HEAD || echo "$BASE_SHA")"
DIRTY="$(git_q status --porcelain)"
COMMIT_COUNT=0
if [[ -n "$DIRTY" ]]; then
  git_q add -A
  if git -C "$WORKTREE" commit -q -m "feat(${TASK_SLUG}): codex draft" >>"$LOG" 2>&1; then
    HEAD_NOW="$(git_q rev-parse HEAD || echo "$HEAD_NOW")"
  fi
fi
# count commits this dispatch produced
if [[ "$HEAD_NOW" != "$BASE_SHA" ]]; then
  COMMIT_COUNT="$(git_q rev-list --count "${BASE_SHA}..${HEAD_NOW}" || echo 1)"
fi

# changed files across the whole dispatch (base..head)
FILES_JSON="[]"
if [[ "$HEAD_NOW" != "$BASE_SHA" ]]; then
  mapfile -t _f < <(git_q diff --name-only "${BASE_SHA}" "${HEAD_NOW}")
  if [[ ${#_f[@]} -gt 0 ]]; then
    FILES_JSON="$(printf '%s\n' "${_f[@]}" | sed 's/\\/\\\\/g; s/"/\\"/g' | awk 'BEGIN{printf "["} {printf "%s\"%s\"", (NR>1?",":""), $0} END{printf "]"}')"
  fi
fi

OK=false
if [[ $RC -eq 0 && "$HEAD_NOW" != "$BASE_SHA" ]]; then
  OK=true
elif [[ $RC -eq 0 && "$HEAD_NOW" == "$BASE_SHA" ]]; then
  DETAIL="codex returned ok but produced NO changes (no commit, clean tree)"
fi

emit "{\"ok\":$OK,\"baseSha\":\"$BASE_SHA\",\"headSha\":\"$HEAD_NOW\",\"files\":$FILES_JSON,\"commitCount\":$COMMIT_COUNT,\"detail\":\"$(jstr "$DETAIL")\"}"
