#!/usr/bin/env bash
# ronin-local — send a bulk/mechanical subtask to Ollama (OpenAI-compatible, localhost).
set -euo pipefail
BASE="${OLLAMA_BASE:-http://localhost:11434/v1}"
MODEL="${RONIN_LOCAL_MODEL:-gemma4:4b}"
TEMP="${RONIN_LOCAL_TEMP:-0.2}"; MAXTOK="${RONIN_LOCAL_MAXTOK:-2048}"
PROMPT="${1:-}"; STDIN_CTX=""
if [ ! -t 0 ]; then STDIN_CTX="$(cat)"; fi
[ -z "$PROMPT$STDIN_CTX" ] && { echo "ronin-local: empty prompt" >&2; exit 1; }
read -r -d '' SYS <<'SYSEOF' || true
You are a fast local worker for mechanical subtasks (summarize, normalize, classify,
reformat, draft boilerplate). Be precise and terse. Output only the requested artifact.
SYSEOF

# Cloud fallback for when the local model is unavailable (Ollama down/not started).
# The caller contract is "run bulk work through ronin-local" (meditation_cycle.md); this
# keeps that contract intact by transparently routing to the CHEAPEST cloud tier instead
# of exiting non-zero and dropping the subtask. Set RONIN_LOCAL_FALLBACK='' to disable
# (restore hard exit 2). Bulk work stays cheap; local is still preferred when up.
# `-` not `:-` so an explicit RONIN_LOCAL_FALLBACK='' disables (restores hard exit 2);
# unset falls back to haiku.
FALLBACK_MODEL="${RONIN_LOCAL_FALLBACK-haiku}"
cloud_fallback() {
  [ -z "$FALLBACK_MODEL" ] && { echo "ronin-local: Ollama unreachable at $BASE (no fallback)" >&2; exit 2; }
  echo "ronin-local: Ollama unreachable at $BASE — falling back to cloud model $FALLBACK_MODEL" >&2
  local user_msg="$PROMPT"
  [ -n "$STDIN_CTX" ] && user_msg="$PROMPT"$'\n\n---\n'"$STDIN_CTX"
  # Prompt goes as the -p argument (stdin is NOT read as the prompt in headless mode).
  # Cheap tier, no tools — same "mechanical worker" contract as local.
  claude --print --model "$FALLBACK_MODEL" -p "$SYS"$'\n\n'"$user_msg" 2>/dev/null \
    || { echo "ronin-local: cloud fallback ($FALLBACK_MODEL) also failed" >&2; exit 2; }
}

PAYLOAD="$(jq -n --arg m "$MODEL" --arg s "$SYS" --arg p "$PROMPT" --arg c "$STDIN_CTX" \
  --argjson t "$TEMP" --argjson mt "$MAXTOK" '{model:$m,temperature:$t,max_tokens:$mt,stream:false,
    messages:[{role:"system",content:$s},{role:"user",content:($p+(if $c=="" then "" else "\n\n---\n"+$c end))}]}')"
RESP="$(curl -sS --fail-with-body --max-time 300 -H 'Content-Type: application/json' -d "$PAYLOAD" "$BASE/chat/completions")" \
  || { cloud_fallback; exit 0; }

# HTTP 200 does NOT mean we got an answer. Ollama returns 200 with an empty (or
# absent) message.content for thinking builds — the documented failure that left
# this tier silently dead for a month — and can return a 2xx {"error": ...} body.
# `jq -r '... // empty'` prints nothing and exits 0, so the old one-liner turned
# "the model never answered" into empty stdout + exit 0. Callers are ronins reading
# prose (prompts/ronin_sword.md routes secret scanning and CVE lookup here), and an
# empty successful result is indistinguishable from "no secrets found".
#
# Mirrors agentica_core/llm/local_guards.py::extract_message_text (content ->
# reasoning -> thinking), which is the repo's mandated guard but is unreachable
# from a shell path. Keep the fallback order in sync with it.
# NOT `.content // .reasoning`: jq's `//` only falls through on null/false, and an
# empty STRING is truthy — so the alternative operator would hand back "" and never
# reach the fallback, which is precisely the thinking-model case this exists for.
# Collect the three candidates, drop any that are absent or blank, take the first.
OUT="$(echo "$RESP" | jq -r '[.choices[0].message | .content, .reasoning, .thinking]
  | map(select(type == "string" and test("\\S")))
  | first // empty')"
if [ -z "${OUT//[[:space:]]/}" ]; then
  ERR="$(echo "$RESP" | jq -r '.error.message // .error // empty' 2>/dev/null || true)"
  echo "ronin-local: model returned no content (model=$MODEL)${ERR:+ — $ERR}" >&2
  echo "ronin-local: treat this as FAILURE, not an empty/clean result" >&2
  exit 2
fi
printf '%s\n' "$OUT"
