#!/bin/bash
# na.sh — dispatch the FREE north coder in ONE fail-closed command. north's `ca.sh` twin.
# audience: AI coding agents first. Invoke by path; do NOT re-derive the env block or the claude -p line.
#
# WHY this exists: the north rig was inline prose in the skill — 6 `export`s + a bounded `claude -p` —
# retyped every dispatch. That is the prose-as-executable hazard (re-derivation = reformulation = the
# `ccr start &` hang class). One tested wrapper removes the re-derivation AND scopes the OpenRouter env
# to this subprocess, so it never pollutes the orchestrator's real-Anthropic shell.
#
# CONTRACT: na.sh --workspace <dir> --trust "<prompt>" --task-slug <slug> [--timeout <secs>]
#   - Ensures ccr is up via the TESTED ccr-up.sh (fail-closed: proxy down -> exit 3, NO dispatch).
#   - cd <workspace> (SHORT path — long absolute CWD triggers north's phantom-tree write bug).
#   - Runs north 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 ca.sh, so the
#     cost-mining tooling finds both). First lines = TOOL + CMD header.
#   - Exit: north's own rc (0 ok; 124 = timeout/non-completion -> orchestrator retries ONCE or 429-waits).
#     2 = usage error; 3 = ccr down OR agent returned 0 with empty captured output (silent write failure /
#     disk full — success requires evidence). north NEVER fixes/gates — that is cursor (ca.sh) + opus.
set -uo pipefail

WORKSPACE="" PROMPT="" TASK_SLUG="" PROFILE="" TIMEOUT=360 SESSION_ID="" CLAUDE_BIN_ARG=""
MODE="dispatch"
INSPECT=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;;
    --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;;
    --session-id) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--session-id requires a value"}' >&2; exit 2; }; SESSION_ID="${2:-}"; shift 2;;
    --claude-bin) [[ $# -lt 2 ]] && { echo '{"ok":false,"detail":"--claude-bin requires a value"}' >&2; exit 2; }; CLAUDE_BIN_ARG="${2:-}"; shift 2;;
    --inspect) INSPECT=1; shift;;
    --health) MODE="health"; shift;;
    --list-models) MODE="list-models"; shift;;
    --interactive) MODE="interactive"; shift;;
    *) echo "{\"ok\":false,\"detail\":\"unknown flag: $1\"}" >&2; exit 2;;
  esac
done
[[ -n "$PROFILE" ]] && echo "notice: --profile=$PROFILE ignored — na.sh has no account routing" >&2

PASSWD_ENTRY="$(/usr/bin/getent passwd "$(/usr/bin/id -u)")" || true
IFS=: read -r _ _ _ _ _ OWNER_HOME _ <<< "$PASSWD_ENTRY"
[[ "$OWNER_HOME" == /* && -d "$OWNER_HOME" ]] || { echo '{"ok":false,"detail":"owner home resolution failed"}' >&2; exit 3; }
TRUSTED_CLAUDE_BIN="$OWNER_HOME/.claude/bin/claude"
CLAUDE_BIN="${CLAUDE_BIN_ARG:-$TRUSTED_CLAUDE_BIN}"
[[ "$CLAUDE_BIN" == "$TRUSTED_CLAUDE_BIN" && -x "$CLAUDE_BIN" ]] || { echo '{"ok":false,"detail":"--claude-bin must be the trusted owner entrypoint"}' >&2; exit 3; }
export HOME="$OWNER_HOME"
export PATH="/usr/local/bin:/usr/bin:/bin:$OWNER_HOME/.local/bin:$OWNER_HOME/.claude/bin"

SCRIPT_DIR="$(cd -- "${BASH_SOURCE[0]%/*}" && pwd -P)"
DEPLOY_ROOT="$(cd "$SCRIPT_DIR/../../../../.." && pwd -P)"
HARNESS_WRAPPERS="$DEPLOY_ROOT/modules/harness/wrappers"
# shellcheck source=/dev/null
source "$HARNESS_WRAPPERS/lib/finalize.sh"
# shellcheck source=/dev/null
source "$HARNESS_WRAPPERS/lib/spend-cap.sh"
_CCR_UP="$DEPLOY_ROOT/modules/workstation/claude/workflows/lib/ccr-up.sh"

load_north_env() {
  # shellcheck source=/dev/null
  source "$SCRIPT_DIR/NORTH.env"
}

ccr_up() {
  local ccr_json
  ccr_json="$(/bin/bash "$_CCR_UP" north 2>/dev/null)"
  case "$ccr_json" in
    *'"up":true'*) return 0 ;;
    *) echo "{\"ok\":false,\"detail\":\"ccr not up: ${ccr_json}\"}" >&2; return 3 ;;
  esac
}

fetch_json() {
  local url="$1"
  URL="$url" /usr/bin/python3 - <<'PY'
import json
import os
import sys
import urllib.error
import urllib.request

url = os.environ.get("URL")
if not url:
    print('{"ok":false,"detail":"missing URL"}', file=sys.stderr)
    sys.exit(2)

try:
    with urllib.request.urlopen(url, timeout=10) as response:
        body = response.read().decode("utf-8")
        if not body.strip():
            body = json.dumps({"ok": True, "url": url, "status": response.status})
        print(body)
except urllib.error.HTTPError as exc:
    detail = {"ok": False, "detail": f"HTTP {exc.code} from {url}"}
    print(json.dumps(detail), file=sys.stderr)
    sys.exit(3)
except Exception as exc:
    detail = {"ok": False, "detail": f"request failed for {url}: {exc}"}
    print(json.dumps(detail), file=sys.stderr)
    sys.exit(3)
PY
}

case "$MODE" in
  health)
    ccr_up || exit $?
    load_north_env || { echo '{"ok":false,"detail":"NORTH.env source failed — engine precondition unmet"}' >&2; exit 3; }
    fetch_json "${_NA_HEALTH_URL:-${ANTHROPIC_BASE_URL%/}/}"
    exit $?
    ;;
  list-models)
    ccr_up || exit $?
    load_north_env || { echo '{"ok":false,"detail":"NORTH.env source failed — engine precondition unmet"}' >&2; exit 3; }
    fetch_json "${_NA_MODELS_URL:-${ANTHROPIC_BASE_URL%/}/v1/models}"
    exit $?
    ;;
  interactive)
    echo '{"ok":false,"detail":"--interactive is not supported by wrappers/na.sh; run claude directly in the workspace"}' >&2
    exit 2
    ;;
esac

# 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)-$$"

[[ -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; }
[[ "$TIMEOUT" =~ ^[1-9][0-9]*$ ]] || { echo '{"ok":false,"detail":"--timeout must be a positive integer"}' >&2; exit 2; }

if [[ $INSPECT -eq 1 ]]; then
  load_north_env || { echo '{"ok":false,"detail":"NORTH.env source failed — engine precondition unmet"}' >&2; exit 3; }
  if [[ -n "$SESSION_ID" ]]; then
    engine_args=(-p "$PROMPT" --session-id "$SESSION_ID" --output-format stream-json --dangerously-skip-permissions)
  else
    engine_args=(-p "$PROMPT" --output-format json --dangerously-skip-permissions)
  fi
  /usr/bin/python3 - "$SCRIPT_DIR/na.sh" "$CLAUDE_BIN" "${engine_args[@]}" <<'PY'
import json
import os
import sys

wrapper, engine, *args = sys.argv[1:]
print(json.dumps({
    "ok": True,
    "purpose": "verification",
    "backend": "north",
    "wrapper": wrapper,
    "engine": engine,
    "model": "cohere/north-mini-code:free",
    "environment": {
        "CLAUDE_CONFIG_DIR": os.environ["CLAUDE_CONFIG_DIR"],
        "ANTHROPIC_BASE_URL": os.environ["ANTHROPIC_BASE_URL"],
        "ANTHROPIC_MODEL": os.environ["ANTHROPIC_MODEL"],
        "ANTHROPIC_SMALL_FAST_MODEL": os.environ["ANTHROPIC_SMALL_FAST_MODEL"],
    },
    "engine_args": args,
}, separators=(",", ":")))
PY
  exit 0
fi

spend_cap_refuse_if_capped north 'north' "$SESSION_ID" '' 'cohere/north-mini-code:free'

# SOURCE OF TRUTH: ~/.claude/skills/north-orchestrator/na.sh - the canonical vendor reference for the north engine wrapper.

# 1. ccr up — TESTED script, fail-closed. north has no point without the proxy.
ccr_up || exit $?

# 2. environment — sourced HERE, scoped to this subprocess (never leaks to the caller).
load_north_env || { echo '{"ok":false,"detail":"NORTH.env source failed — engine precondition unmet"}' >&2; exit 3; }

mkdir -p "$HOME/Projects/mega-plan-harness/tmp/logs"
LOG="$HOME/Projects/mega-plan-harness/tmp/logs/$RUN_ID-$TASK_SLUG.log"
RAW_OUTPUT_FILE="$HOME/Projects/mega-plan-harness/tmp/logs/$RUN_ID-$TASK_SLUG.raw"
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
}

# DRY: build the dispatch ONCE; header line and run line use the same array so they cannot diverge.
OUTPUT_FORMAT="json"
SESSION_STATUS=""
if [[ -n "$SESSION_ID" ]]; then
  OUTPUT_FORMAT="stream-json"
  SESSION_STATUS="=== session_id: $SESSION_ID ==="
  NORTH_ARGS=(/usr/bin/timeout -k 5 "$TIMEOUT" "$CLAUDE_BIN" -p "$PROMPT" --session-id "$SESSION_ID" --output-format "$OUTPUT_FORMAT" --dangerously-skip-permissions)
else
  NORTH_ARGS=(/usr/bin/timeout -k 5 "$TIMEOUT" "$CLAUDE_BIN" -p "$PROMPT" --output-format "$OUTPUT_FORMAT" --dangerously-skip-permissions)
fi
{
  echo "=== TOOL: north (cohere/north-mini-code:free) via ccr ==="
  echo "=== CMD: $(printf '%q ' "${NORTH_ARGS[@]}") ==="
  echo "=== workspace: $WORKSPACE  timeout: ${TIMEOUT}s  log: $LOG ==="
  [[ -n "$SESSION_STATUS" ]] && echo "$SESSION_STATUS"
} | tee "$LOG"

# 3. dispatch FOREGROUND, in the (short) workspace, stdin closed. Never background, never pipe to a pager.
cd "$WORKSPACE" || { echo '{"ok":false,"detail":"cd workspace failed"}' >&2; exit 2; }
"${NORTH_ARGS[@]}" < /dev/null 2>&1 | stream_output
RC="${PIPESTATUS[0]}"
echo "=== north exit=$RC ===" | tee -a "$LOG"
finalize_terminal "$RC" 'north' "$SESSION_ID" '' 'cohere/north-mini-code:free'
