#!/usr/bin/env bash
# autopilot-status.sh  -  what continuous mode is doing, in one place.
#
# ONE producer, three renderings. The menu bar, the terminal and the session hook
# all describe the same thing, and the moment each computes its own answer they
# start disagreeing - which is exactly the failure this release spent its time on:
# a run reading `in_progress` while its PR was already open.
#
#   (default)    a paragraph for a human reading the terminal
#   --short      one line, for autopilot-off and the session hook
#   --json       status.json, which is what bin/menubar renders. Besides the
#                queue it carries the runner's operations: inhibitorHeld,
#                lastGcReport, lastDigest (paths, empty when none),
#                maxParallelAgents (null when unlimited) and
#                awake: {held, display} from the awake agent.
#   --write      compute and write status.json, print nothing
#   --subjects   one line per in-flight item for the native task widget
#
# `--json` and `--write` produce the SAME bytes. A menu bar fed by a different
# code path than the terminal is a second source of truth with a nicer font.
#
# Every line a person reads renders in `prefs.global.outputLanguage`, including
# the ones that travel to the menu bar inside status.json: this script is the
# only place that wording exists, so the corner of the screen and the terminal
# cannot say it two ways.
#
# Exit: 0 always when it can read the state, 2 on usage, 3 when --json or
# --write cannot build the envelope (jq missing, or jq rejecting an input;
# --json still prints a JSON {"error": ...} object, --write leaves the previous
# status.json in place). Never non-zero because the mode is off - "off" is an
# answer.

set -uo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=/dev/null
if [ -f "$HERE/../lib/autopilot-state.sh" ]; then . "$HERE/../lib/autopilot-state.sh"; else . "$HOME/.claude/lib/autopilot-state.sh"; fi

MODE="human"
case "${1:-}" in
  "") ;;
  --short) MODE="short" ;;
  --json) MODE="json" ;;
  --write) MODE="write" ;;
  --subjects) MODE="subjects" ;;
  -h | --help)
    grep -E '^#( |$)' "$0" | sed -E 's/^# ?//'
    exit 0
    ;;
  *)
    echo "autopilot-status: unknown option $1" >&2
    exit 2
    ;;
esac

# A decoder reads stdout, so under --json even this failure is JSON, and the exit
# code says the envelope is not a status.
if ! command -v jq >/dev/null 2>&1; then
  case "$MODE" in
    json) printf '%s\n' '{"error":"jq-required","message":"autopilot-status: jq is required"}'; exit 3 ;;
    write) echo "autopilot-status: jq is required" >&2; exit 3 ;;
    *) echo "autopilot-status: jq is required"; exit 0 ;;
  esac
fi

# The same resolver every other shipped script reads, so a run whose preferences
# are pointed at a fixture renders this surface and the arming verdict inside it
# from one file. The codes are the schema's, lowercase and exact.
OUT_LANG=$(ma_output_lang 2>/dev/null || echo en)
case "$OUT_LANG" in tr | en) ;; *) OUT_LANG=en ;; esac

# One table, both languages on the same line, so a wording change cannot land on
# one of them. English is the default and the fallback for a language the table
# does not carry.
msg() { # $1 = key, $2.. = printf arguments
  local key="$1" en='' tr='' fmt=''
  shift
  case "$key" in
    power-ac)
      en='plugged in, sleep is held while a run is in flight'
      tr='prizde, bir koşu sürerken uyku engelleniyor' ;;
    power-battery)
      en='on battery, sleep is free - it continues when you plug in'
      tr='bataryada, uyku serbest - prize takınca devam eder' ;;
    awake-held)
      en='kept awake while autopilot is on - system sleep held on AC, idle sleep on battery too'
      tr='autopilot açıkken uyanık tutuluyor - prizde sistem uykusu, bataryada da boşta uyku engelleniyor' ;;
    awake-held-display)
      en='kept awake while autopilot is on, display included'
      tr='autopilot açıkken ekran dahil uyanık tutuluyor' ;;
    orphaned)
      en='the config is on but launchd holds no job - re-arm with %s'
      tr='config açık ama launchd bir iş tutmuyor - %s ile yeniden kur' ;;
    phase-word)
      en='Phase'
      tr='Faz' ;;
    awaiting-subject)
      en='waiting for an answer'
      tr='cevap bekliyor' ;;
    short-off)
      en='autopilot: off (%s repos selected)'
      tr='autopilot: kapalı (%s repo seçili)' ;;
    short-absent)
      en='autopilot: not set up'
      tr='autopilot: kurulu değil' ;;
    short-on)
      en='autopilot: on · %s running, %s queued, %s awaiting an answer · %s'
      tr='autopilot: açık · %s koşuyor, %s sırada, %s cevap bekliyor · %s' ;;
    human-absent)
      en='autopilot: not set up on this machine.'
      tr='autopilot: bu makinede kurulu değil.' ;;
    human-turn-on)
      en='  To turn it on: %s'
      tr='  Açmak için: %s' ;;
    human-on)
      en='autopilot: ON · %s repos · %s slots (this machine holds %s) · scanning every %ss'
      tr='autopilot: AÇIK · %s repo · %s slot (bu makine %s kaldırır) · her %ssn tarıyor' ;;
    human-off)
      en='autopilot: OFF · %s repos selected, nothing scheduled'
      tr='autopilot: KAPALI · %s repo seçili, zamanlama yok' ;;
    heading-running)
      en='Running (%s):'
      tr='Koşuyor (%s):' ;;
    heading-queued)
      en='Queued (%s):'
      tr='Sırada (%s):' ;;
    heading-awaiting)
      en='Waiting for an answer (%s) - the open questions are posted, and it continues once they are answered:'
      tr='Cevap bekliyor (%s) - açık soruları yorumladık, cevap gelince devam edecek:' ;;
    queue-empty)
      en='Nothing queued.'
      tr='Sırada iş yok.' ;;
    queue-empty-hint)
      en='  To give it an item: add the label to a GitHub issue, or to a Jira item assigned to you.'
      tr='  Bir madde alması için: GitHub issue veya sana atanmış Jira maddesine label ekle.' ;;
    blocked)
      en='Not taking new items: %s'
      tr='Yeni madde alınmıyor: %s' ;;
    spend)
      en='Last 24 hours: $%s / $%s'
      tr='Son 24 saat: $%s / $%s' ;;
    heading-reports)
      en='Last 24 hours (%s PRs):'
      tr='Son 24 saat (%s PR):' ;;
    *)
      printf 'autopilot-status: no message for %s\n' "$key" >&2
      return 1 ;;
  esac
  # English is the fallback per key, not only per language: a key whose Turkish
  # half is absent renders its English one rather than a bare newline.
  if [ "$OUT_LANG" = tr ] && [ -n "$tr" ]; then fmt="$tr"; else fmt="$en"; fi
  # shellcheck disable=SC2059
  printf "$fmt\n" "$@"
}

ON=false; ma_ap_is_on && ON=true
CONFIGURED=false; ma_ap_is_configured && CONFIGURED=true
POWER=$(ma_ap_power)
SLOTS=$(ma_ap_cfg '.slots' 1)
CEILING=$(ma_ap_slot_ceiling)
INTERVAL=$(ma_ap_cfg '.scanIntervalSeconds' 120)

QUEUE=$(ma_ap_read queue.json 2>/dev/null || echo '{}')
RUNNING=$(printf '%s' "$QUEUE" | jq -c '[.running[]? | select((.state // "running") == "running")]' 2>/dev/null || echo '[]')
QUEUED=$(printf '%s' "$QUEUE" | jq -c '[.queued[]?]' 2>/dev/null || echo '[]')
AWAITING=$(printf '%s' "$QUEUE" | jq -c '[.awaiting[]?]' 2>/dev/null || echo '[]')
LAST_TICK=$(printf '%s' "$QUEUE" | jq -r '.lastTickAt // empty' 2>/dev/null)
EMPTY_REASON=$(printf '%s' "$QUEUE" | jq -r '.emptyReason // empty' 2>/dev/null)
BLOCKED=$(printf '%s' "$QUEUE" | jq -r '.blockedReason // empty' 2>/dev/null)

# Spend and the arming verdict come from the one script that computes them, so
# the terminal, the menu bar and the runner cannot disagree about whether the
# queue is allowed to take an item.
ARM='{}'
ARMING=$(ma_ap_asset scripts/autopilot-arming.mjs 2>/dev/null || true)
if [ "$CONFIGURED" = true ] && [ -n "$ARMING" ]; then
  ARM=$(node "$ARMING" check --json 2>/dev/null || echo '{}')
fi
SPEND=$(printf '%s' "$ARM" | jq -r '.spend.usd // 0' 2>/dev/null || echo 0)
CEILING_USD=$(printf '%s' "$ARM" | jq -r '.ceiling // 0' 2>/dev/null || echo 0)

# Recently finished, from the append-only attempt log rather than from a second
# list somebody has to remember to prune. Bounded to a day: this answers "what
# happened while I was away", and an unbounded list stops answering that.
CUTOFF=$(($(date +%s) - 86400))
REPORTS='[]'
if [ -f "$(ma_ap_root)/attempted.jsonl" ]; then
  REPORTS=$(jq -sc --argjson c "$CUTOFF" '
    [ .[] | select((.at // 0) >= $c) | select(.prUrl != null and .prUrl != "")
      | {id: .id, kind: "pr", url: .prUrl, outcome: (.outcome // "open"), finishedAt: .at} ]
    | sort_by(-.finishedAt) | .[0:10]' "$(ma_ap_root)/attempted.jsonl" 2>/dev/null || echo '[]')
fi
TODAY_N=$(printf '%s' "$REPORTS" | jq 'length' 2>/dev/null || echo 0)

# The operations the runner owns. The inhibitor is held when the pid its
# record names is alive; a record left by a runner that died is not a held lock.
AP_ROOT=$(ma_ap_root)
INHIBITOR_HELD=false
INH_PID=$(jq -r '.pid // empty' "$AP_ROOT/inhibitor.json" 2>/dev/null || true)
case "$INH_PID" in
  "" | *[!0-9]*) ;;
  *) kill -0 "$INH_PID" 2>/dev/null && INHIBITOR_HELD=true ;;
esac
latest_daily() { # $1 = prefix; the newest <prefix><date>.json, or empty
  local f last=""
  for f in "$AP_ROOT/$1"*.json; do [ -f "$f" ] && last="$f"; done
  printf '%s' "$last"
}
LAST_GC=$(latest_daily gc-report-)
LAST_DIGEST=$(latest_daily digest-)
# The awake agent: the launchd job that holds sleep off for as long as the mode
# is on, read by the same script that installs and removes it.
AWAKE='{"held":false,"display":false}'
AWAKE_SCRIPT=$(ma_ap_asset scripts/autopilot-awake.mjs 2>/dev/null || true)
if [ -n "$AWAKE_SCRIPT" ]; then
  AWAKE_OUT=$(node "$AWAKE_SCRIPT" status --json 2>/dev/null || true)
  if printf '%s' "$AWAKE_OUT" | jq -e '(.held|type=="boolean") and (.display|type=="boolean")' >/dev/null 2>&1; then
    AWAKE=$(printf '%s' "$AWAKE_OUT" | jq -c '{held, display}')
  fi
fi
AWAKE_HELD=$(printf '%s' "$AWAKE" | jq -r '.held')
AWAKE_DISPLAY=$(printf '%s' "$AWAKE" | jq -r '.display')
MAX_PAR=$(ma_ap_cfg '.maxParallelAgents' null 2>/dev/null || echo null)
case "$MAX_PAR" in "" | *[!0-9]*) MAX_PAR=null ;; esac
TODAY_USD=$(printf '%s' "$QUEUE" | jq -r '.todayUsd // 0' 2>/dev/null || echo 0)

NOTE=""
if [ "$ON" = true ]; then
  if [ "$AWAKE_HELD" = true ] && [ "$AWAKE_DISPLAY" = true ]; then NOTE=$(msg awake-held-display)
  elif [ "$AWAKE_HELD" = true ]; then NOTE=$(msg awake-held)
  elif [ "$POWER" = "ac" ]; then NOTE=$(msg power-ac)
  else NOTE=$(msg power-battery); fi
fi
if ma_ap_is_orphaned; then
  NOTE=$(msg orphaned "$(ma_ap_command autopilot-on)")
fi

STOP_COMMAND=$(ma_ap_command autopilot-off)

build_json() {
  jq -nc \
    --argjson on "$ON" --argjson slots "$SLOTS" \
    --argjson running "$RUNNING" --argjson queued "$QUEUED" --argjson awaiting "$AWAITING" \
    --argjson reports "$REPORTS" --argjson todayCount "$TODAY_N" --argjson todayUsd "$TODAY_USD" \
    --arg power "$POWER" --arg note "$NOTE" --arg stopCommand "$STOP_COMMAND" \
    --arg emptyReason "$EMPTY_REASON" --arg blockedReason "$BLOCKED" \
    --argjson interval "$INTERVAL" --argjson spendUsd "${SPEND:-0}" \
    --argjson ceilingUsd "${CEILING_USD:-0}" \
    --argjson lastTickAt "${LAST_TICK:-null}" \
    --argjson inhibitorHeld "$INHIBITOR_HELD" --arg lastGcReport "$LAST_GC" \
    --arg lastDigest "$LAST_DIGEST" --argjson maxParallelAgents "$MAX_PAR" \
    --argjson awake "$AWAKE" \
    '{on:$on, slots:$slots, running:$running, queued:$queued, awaiting:$awaiting,
      reports:$reports, todayCount:$todayCount, todayUsd:$todayUsd,
      lastTickAt:$lastTickAt, power:$power, note:$note, stopCommand:$stopCommand,
      scanIntervalSeconds:$interval, emptyReason:$emptyReason,
      blockedReason:$blockedReason, spendUsd:$spendUsd, ceilingUsd:$ceilingUsd,
      inhibitorHeld:$inhibitorHeld, lastGcReport:$lastGcReport, lastDigest:$lastDigest,
      maxParallelAgents:$maxParallelAgents, awake:$awake}'
}

# The envelope, or nothing when jq could not build a JSON object from the inputs
# (a config value of the wrong type, for one). status.json is only ever replaced
# by a complete envelope, so the menu bar keeps its last good answer.
ENVELOPE=$(build_json 2>/dev/null) || ENVELOPE=""
printf '%s' "$ENVELOPE" | jq -e 'type == "object"' >/dev/null 2>&1 || ENVELOPE=""

case "$MODE" in
  json)
    if [ -z "$ENVELOPE" ]; then
      printf '%s\n' '{"error":"envelope-failed","message":"autopilot-status: could not build the status envelope"}'
      exit 3
    fi
    printf '%s\n' "$ENVELOPE"
    exit 0
    ;;
  write)
    if [ -z "$ENVELOPE" ]; then
      echo "autopilot-status: could not build the status envelope; status.json left unchanged" >&2
      exit 3
    fi
    printf '%s\n' "$ENVELOPE" | ma_ap_write status.json || exit 3
    exit 0
    ;;
esac

# Refresh status.json as a side effect of being asked, so the menu bar is current
# the moment anyone looks at the terminal - but ONLY when the mode is already
# configured. Asking a question may not answer it into existence: on a machine
# that never turned this on, `ma_ap_write` would create ~/.claude/autopilot/ and
# quietly break the guarantee that a clean install writes nothing. Caught by
# running this file on this machine, which then had the directory.
if [ "$CONFIGURED" = true ] && [ -n "$ENVELOPE" ]; then
  printf '%s\n' "$ENVELOPE" | ma_ap_write status.json 2>/dev/null || true
fi

NR=$(printf '%s' "$RUNNING" | jq 'length')
NQ=$(printf '%s' "$QUEUED" | jq 'length')
NA=$(printf '%s' "$AWAITING" | jq 'length')

if [ "$MODE" = "subjects" ]; then
  # One line per item, with the numbers INSIDE the string: the widget takes
  # exactly one string per row, so anything the reader needs has to travel in it.
  printf '%s' "$RUNNING" | jq -r --arg phase "$(msg phase-word)" '.[] |
    "\(.id) · \($phase) \(.phase // "?")\(if .phaseTotal then "/\(.phaseTotal)" else "" end) \(.phaseName // "") · \(.stack // "")"'
  printf '%s' "$AWAITING" | jq -r --arg awaiting "$(msg awaiting-subject)" '.[] | "\(.id) · \($awaiting)"'
  exit 0
fi

if [ "$MODE" = "short" ]; then
  if [ "$ON" != true ]; then
    if [ "$CONFIGURED" = true ]; then
      msg short-off "$(ma_ap_cfg '.repos | length' 0)"
    else
      msg short-absent
    fi
    exit 0
  fi
  msg short-on "$NR" "$NQ" "$NA" "$NOTE"
  exit 0
fi

# --- human -------------------------------------------------------------------
if [ "$CONFIGURED" != true ]; then
  msg human-absent
  msg human-turn-on "$(ma_ap_command autopilot-on)"
  exit 0
fi

NREPOS=$(ma_ap_cfg '.repos | length' 0)
if [ "$ON" = true ]; then
  msg human-on "$NREPOS" "$SLOTS" "$CEILING" "$INTERVAL"
else
  msg human-off "$NREPOS"
  msg human-turn-on "$(ma_ap_command autopilot-on)"
fi
[ -n "$NOTE" ] && echo "  $NOTE"

echo
if [ "$NR" -gt 0 ]; then
  msg heading-running "$NR"
  printf '%s' "$RUNNING" | jq -r '.[] | "  \(.id) · \(.phaseName // .phase // "?") · \(.stack // "")"'
fi
if [ "$NQ" -gt 0 ]; then
  msg heading-queued "$NQ"
  printf '%s' "$QUEUED" | jq -r '.[0:8][] | "  \(.id)"'
fi
if [ "$NA" -gt 0 ]; then
  msg heading-awaiting "$NA"
  printf '%s' "$AWAITING" | jq -r '.[] | "  \(.id)"'
fi
if [ "$NR" -eq 0 ] && [ "$NQ" -eq 0 ] && [ "$NA" -eq 0 ]; then
  msg queue-empty
  # The single most common "why is nothing happening" answer, and it is not a
  # fault: no item carries the label yet. When intake recorded a more specific
  # reason, that one is truer and goes first.
  if [ -n "$EMPTY_REASON" ]; then
    echo "  $EMPTY_REASON"
  fi
  msg queue-empty-hint
fi

# The queue refusing to take work is not the same as having none, and reading
# one as the other is the whole reason this line exists separately.
if [ -n "$BLOCKED" ]; then
  echo
  msg blocked "$BLOCKED"
fi
if [ "$CEILING_USD" != "0" ]; then
  msg spend "$SPEND" "$CEILING_USD"
fi

if [ "$TODAY_N" -gt 0 ]; then
  echo
  msg heading-reports "$TODAY_N"
  printf '%s' "$REPORTS" | jq -r '.[0:6][] | "  \(.id) · \(.url)"'
fi
exit 0
