#!/bin/sh
set -eu

DEFAULT_SLUG="supporter-channel-qa"
FUCR_RELEASE_VERSION="0.6.0"
FU_AGENT_EXPLICIT=0
[ "${FU_AGENT+x}" ] && FU_AGENT_EXPLICIT=1
FUCR_WORKSPACE_ROOT_EXPLICIT=0
if [ "${FUCR_WORKSPACE_ROOT+x}" ] || [ "${FU_WORKSPACE_ROOT+x}" ]; then
  FUCR_WORKSPACE_ROOT_EXPLICIT=1
fi
FU_BASE_URL_EXPLICIT=0
if [ -n "${FU_BASE_URL:-}" ] || [ -n "${FUCR_BASE_URL:-}" ] || [ -n "${U_BASE_URL:-}" ]; then
  FU_BASE_URL_EXPLICIT=1
fi
if [ -z "${FU_BASE_URL:-}" ] && [ -n "${FUCR_BASE_URL:-}" ]; then
  FU_BASE_URL="$FUCR_BASE_URL"
fi
if [ -z "${FU_BASE_URL:-}" ] && [ -n "${U_BASE_URL:-}" ]; then
  printf "Using U_BASE_URL as FU_BASE_URL. If this was accidental, paste the command on one line or keep the backslash before the line break.\n" >&2
  FU_BASE_URL="$U_BASE_URL"
fi
FU_BASE_URL="${FU_BASE_URL:-https://www.futureunion.org.uk}"
FUCR_CONFIG_DIR="${FUCR_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/future-union}"
FUCR_PROFILE_FILE="${FUCR_PROFILE_FILE:-$FUCR_CONFIG_DIR/profile.env}"
# BYOK (bring-your-own-key) lane: optional key file, loaded silently when
# present so child processes (fu-mission, pi) inherit e.g. EXA_API_KEY.
# The runner NEVER asks for API keys and never prints key values. Exact file
# shape and rules: ops/compute-lanes.md. Mirrors load_byok_keys in
# bin/fu-mission — keep the two in sync.
FU_BYOK_KEYS_FILE="${FU_BYOK_KEYS_FILE:-$FUCR_CONFIG_DIR/keys.env}"
FU_BYOK_ACTIVE=0
FU_BYOK_KEY_COUNT=0
load_byok_keys() {
  [ -f "$FU_BYOK_KEYS_FILE" ] || return 0
  while IFS= read -r byok_line || [ -n "$byok_line" ]; do
    case "$byok_line" in
      ""|\#*) continue ;;
    esac
    byok_name="${byok_line%%=*}"
    byok_value="${byok_line#*=}"
    [ "$byok_name" = "$byok_line" ] && continue
    case "$byok_name" in
      [A-Z]*) ;;
      *) continue ;;
    esac
    case "$byok_name" in
      *[!A-Z0-9_]*) continue ;;
    esac
    case "$byok_name" in
      *_API_KEY|*_API_TOKEN|*_KEY) ;;
      *) continue ;;
    esac
    [ -n "$byok_value" ] || continue
    if eval "[ -z \"\${$byok_name:-}\" ]"; then
      export "$byok_name=$byok_value"
    fi
    FU_BYOK_KEY_COUNT=$((FU_BYOK_KEY_COUNT + 1))
    FU_BYOK_ACTIVE=1
  done < "$FU_BYOK_KEYS_FILE"
  return 0
}
load_byok_keys
FU_PROFILE_PROMPT="${FU_PROFILE_PROMPT:-ask}"
FUCR_CACHE_DIR="${FUCR_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/future-union/fucr}"
FUCR_BIN_DIR="${FUCR_BIN_DIR:-$HOME/.local/bin}"
FUCR_WORKSPACE_ROOT="${FUCR_WORKSPACE_ROOT:-${FU_WORKSPACE_ROOT:-}}"
if [ -z "$FUCR_WORKSPACE_ROOT" ]; then
  if [ -d "$HOME/dev" ]; then
    FUCR_WORKSPACE_ROOT="$HOME/dev/future-union-mission"
  else
    FUCR_WORKSPACE_ROOT="$HOME/future-union-mission"
  fi
fi
FUCR_INSTALL="${FUCR_INSTALL:-1}"
FUCR_UPDATE="${FUCR_UPDATE:-0}"
FUCR_TUI_CHILD="${FUCR_TUI_CHILD:-0}"
FU_FORCE="${FU_FORCE:-0}"
FU_AGENT="${FU_AGENT:-auto}"
FU_AGENT_CMD="${FU_AGENT_CMD:-}"
FU_START="${FU_START:-ask}"
FU_PICK="${FU_PICK:-1}"
# Plain shell is the golden path: no Rust, no cargo, no TUI unless you opt in
# with FU_TUI=ratatui (or lite/textual/node).
FU_TUI="${FU_TUI:-0}"
FU_RADICLE="${FU_RADICLE:-0}"
FU_YES="${FU_YES:-0}"
FU_BOOTSTRAP="${FU_BOOTSTRAP:-1}"
FU_DRY_RUN="${FU_DRY_RUN:-0}"
FU_RADICLE_SETUP="${FU_RADICLE_SETUP:-ask}"
FU_PI_UPDATE="${FU_PI_UPDATE:-ask}"
FU_ENABLE_PI_SANDBOX="${FU_ENABLE_PI_SANDBOX:-0}"
# Pinned Pi extension profile. Keep in sync with /agent-pack/pi-profile.json
# (fu-pi-profile). Versioned npm specs are pinned by Pi and skipped by
# `pi update --extensions`, so these survive routine updates.
FU_PI_PROFILE_VERSION="1.0.0"
FU_PI_CORE_PINS="npm:pi-autoresearch@1.6.1 npm:pi-web-access@0.13.0"
FU_PI_SANDBOX_PIN="npm:pi-landstrip@0.16.22"
FU_CONTRIBUTOR_NAME="${FU_CONTRIBUTOR_NAME:-}"
FU_CONTRIBUTOR_HANDLE="${FU_CONTRIBUTOR_HANDLE:-}"
FU_CONTRIBUTOR_TEAM="${FU_CONTRIBUTOR_TEAM:-}"
FU_CONTACT="${FU_CONTACT:-}"
FU_CREDIT_MODE="${FU_CREDIT_MODE:-ask-before-public}"
FU_ANONYMOUS="${FU_ANONYMOUS:-no}"
FU_IDEA_FILE="${FU_IDEA_FILE:-}"
picker_dir=""
tmp_dir=""

trim_trailing_slashes() {
  printf "%s" "$1" | sed 's:/*$::'
}

fail() {
  printf "%s\n" "$1" >&2
  exit 1
}

# Every branch must end with return 0: under set -eu a false one-liner guard
# as the last command makes this function (and load_profile) return 1, which
# silently kills the runner whenever an env var already overrides a saved
# profile value. Mirrors the fixed pattern in bin/fu-mission.
profile_set_if_allowed() {
  key="$1"
  value="$2"
  case "$key" in
    FU_CONTRIBUTOR_NAME)
      if [ -z "$FU_CONTRIBUTOR_NAME" ]; then
        FU_CONTRIBUTOR_NAME="$value"
      fi
      ;;
    FU_CONTRIBUTOR_HANDLE)
      if [ -z "$FU_CONTRIBUTOR_HANDLE" ]; then
        FU_CONTRIBUTOR_HANDLE="$value"
      fi
      ;;
    FU_CONTRIBUTOR_TEAM)
      if [ -z "$FU_CONTRIBUTOR_TEAM" ]; then
        FU_CONTRIBUTOR_TEAM="$value"
      fi
      ;;
    FU_CONTACT)
      if [ -z "$FU_CONTACT" ]; then
        FU_CONTACT="$value"
      fi
      ;;
    FU_CREDIT_MODE)
      if [ "$FU_CREDIT_MODE" = "ask-before-public" ]; then
        FU_CREDIT_MODE="$value"
      fi
      ;;
    FU_AGENT)
      if [ "$FU_AGENT_EXPLICIT" = "0" ]; then
        FU_AGENT="$value"
      fi
      ;;
    FUCR_WORKSPACE_ROOT)
      if [ "$FUCR_WORKSPACE_ROOT_EXPLICIT" = "0" ]; then
        FUCR_WORKSPACE_ROOT="$value"
      fi
      ;;
  esac
  return 0
}

load_profile() {
  [ -f "$FUCR_PROFILE_FILE" ] || return 0
  while IFS='=' read -r key value || [ -n "$key" ]; do
    case "$key" in
      ""|\#*) continue ;;
      FU_CONTRIBUTOR_NAME|FU_CONTRIBUTOR_HANDLE|FU_CONTRIBUTOR_TEAM|FU_CONTACT|FU_CREDIT_MODE|FU_AGENT|FUCR_WORKSPACE_ROOT)
        profile_set_if_allowed "$key" "$value"
        ;;
    esac
  done < "$FUCR_PROFILE_FILE"
  return 0
}

write_profile() {
  mkdir -p "$FUCR_CONFIG_DIR"
  umask 077
  {
    printf "FU_CONTRIBUTOR_NAME=%s\n" "$FU_CONTRIBUTOR_NAME"
    printf "FU_CONTRIBUTOR_HANDLE=%s\n" "$FU_CONTRIBUTOR_HANDLE"
    printf "FU_CONTRIBUTOR_TEAM=%s\n" "$FU_CONTRIBUTOR_TEAM"
    printf "FU_CONTACT=%s\n" "$FU_CONTACT"
    printf "FU_CREDIT_MODE=%s\n" "$FU_CREDIT_MODE"
    printf "FU_AGENT=%s\n" "$FU_AGENT"
    printf "FUCR_WORKSPACE_ROOT=%s\n" "$FUCR_WORKSPACE_ROOT"
  } > "$FUCR_PROFILE_FILE"
  chmod 600 "$FUCR_PROFILE_FILE" 2>/dev/null || true
}

profile_summary() {
  printf "Profile: %s\n" "$FUCR_PROFILE_FILE"
  if [ -f "$FUCR_PROFILE_FILE" ]; then
    printf "Status: saved\n"
  else
    printf "Status: not saved\n"
  fi
  printf "Contributor: %s\n" "${FU_CONTRIBUTOR_NAME:-anonymous until set}"
  printf "Handle: %s\n" "${FU_CONTRIBUTOR_HANDLE:-unset}"
  printf "Team: %s\n" "${FU_CONTRIBUTOR_TEAM:-unset}"
  printf "Review contact: %s\n" "${FU_CONTACT:-unset}"
  printf "Credit mode: %s\n" "$FU_CREDIT_MODE"
  printf "Default agent: %s\n" "$FU_AGENT"
  printf "Workspace root: %s\n" "$FUCR_WORKSPACE_ROOT"
}

read_profile_field() {
  label="$1"
  current="$2"
  printf "%s [%s]: " "$label" "${current:-blank}" >&2
  IFS= read -r answer
  if [ -n "$answer" ]; then
    printf "%s" "$answer"
  else
    printf "%s" "$current"
  fi
}

edit_profile() {
  if [ ! -t 0 ]; then
    fail "Profile editing needs an interactive terminal. Set FU_CONTRIBUTOR_NAME/FU_CONTRIBUTOR_HANDLE/FU_CONTACT instead."
  fi

  printf "\nFuture Union contributor profile\n"
  printf "Saved locally at %s. This is used for CREDIT.md and reviewed attribution; it is not published automatically.\n\n" "$FUCR_PROFILE_FILE"
  FU_CONTRIBUTOR_NAME="$(read_profile_field "Name for reviewer credit" "$FU_CONTRIBUTOR_NAME")"
  FU_CONTRIBUTOR_HANDLE="$(read_profile_field "Public handle (optional)" "$FU_CONTRIBUTOR_HANDLE")"
  FU_CONTRIBUTOR_TEAM="$(read_profile_field "Team/group (optional)" "$FU_CONTRIBUTOR_TEAM")"
  FU_CONTACT="$(read_profile_field "Private review contact (optional)" "$FU_CONTACT")"
  FU_CREDIT_MODE="$(read_profile_field "Credit mode named|team|anonymous|ask-before-public" "$FU_CREDIT_MODE")"
  FU_AGENT="$(read_profile_field "Default agent auto|pi|codex|claude|none" "$FU_AGENT")"
  FUCR_WORKSPACE_ROOT="$(read_profile_field "Mission workspace root" "$FUCR_WORKSPACE_ROOT")"

  case "$FU_CREDIT_MODE" in
    named|team|anonymous|ask-before-public) ;;
    *) printf "Unknown credit mode, using ask-before-public.\n" >&2; FU_CREDIT_MODE="ask-before-public" ;;
  esac
  case "$FU_AGENT" in
    auto|pi|py|pyresearch|pi-autoresearch|codex|claude|none) ;;
    *) printf "Unknown agent, using auto.\n" >&2; FU_AGENT="auto" ;;
  esac

  write_profile
  printf "\nSaved Future Union profile.\n"
  profile_summary
}

unset_profile() {
  if [ -f "$FUCR_PROFILE_FILE" ]; then
    rm -f "$FUCR_PROFILE_FILE"
    printf "Removed Future Union profile: %s\n" "$FUCR_PROFILE_FILE"
  else
    printf "No Future Union profile found at %s\n" "$FUCR_PROFILE_FILE"
  fi
}

ensure_profile() {
  case "$FU_PROFILE_PROMPT" in
    0|no|false|skip) return 0 ;;
    1|yes|true|ask) ;;
    *) printf "FU_PROFILE_PROMPT must be ask, yes, or no.\n" >&2; return 1 ;;
  esac

  [ -t 0 ] || return 0
  [ "$FU_YES" = "1" ] && return 0
  if [ -f "$FUCR_PROFILE_FILE" ]; then
    printf "Using Future Union profile: %s / %s. Edit with \`fucr profile edit\`.\n" "${FU_CONTRIBUTOR_NAME:-anonymous}" "$FU_CREDIT_MODE" >&2
    return 0
  fi

  printf "\nSet up contributor credit now? This stays on your machine and helps reviewers credit useful work. [Y/n] "
  read answer
  case "${answer:-Y}" in
    n|N|no|NO)
      mkdir -p "$FUCR_CONFIG_DIR"
      printf "# skipped\n" > "$FUCR_PROFILE_FILE"
      chmod 600 "$FUCR_PROFILE_FILE" 2>/dev/null || true
      printf "Skipped profile setup. Run \`fucr profile edit\` whenever you want credit details saved.\n"
      ;;
    *) edit_profile ;;
  esac
}

profile_command() {
  action="${1:-show}"
  case "$action" in
    show|"") profile_summary ;;
    edit|setup|update) edit_profile ;;
    unset|remove|reset) unset_profile ;;
    path) printf "%s\n" "$FUCR_PROFILE_FILE" ;;
    *)
      printf "Unknown profile command: %s\n" "$action" >&2
      printf "Use: fucr profile show|edit|unset|path\n" >&2
      exit 1
      ;;
  esac
}

mission_feed_valid() {
  candidate_url="$(trim_trailing_slashes "$1")"
  feed_tmp="$(mktemp "${TMPDIR:-/tmp}/future-union-feed.XXXXXX")"
  if ! curl -fsS "$candidate_url/api/mission-computer.json" -o "$feed_tmp" 2>/dev/null; then
    rm -f "$feed_tmp"
    return 1
  fi

  if command -v node >/dev/null 2>&1; then
    node -e '
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
if (!Array.isArray(payload.actions) || !Array.isArray(payload.missions)) {
  process.exit(1);
}
' "$feed_tmp" >/dev/null 2>&1
    feed_status="$?"
  else
    grep -q '"actions"' "$feed_tmp" && grep -q '"missions"' "$feed_tmp"
    feed_status="$?"
  fi
  rm -f "$feed_tmp"
  return "$feed_status"
}

resolve_base_url() {
  if mission_feed_valid "$base_url"; then
    return 0
  fi

  if [ "$FU_BASE_URL_EXPLICIT" = "1" ]; then
    printf "Warning: %s/api/mission-computer.json is not valid Future Union JSON.\n" "$base_url" >&2
    printf "If you are working locally, rerun with FU_BASE_URL=http://127.0.0.1:4392.\n" >&2
    return 0
  fi

  for local_candidate in http://127.0.0.1:4392 http://127.0.0.1:4391 http://127.0.0.1:4386; do
    if mission_feed_valid "$local_candidate"; then
      printf "Using local Future Union dev server: %s\n" "$local_candidate" >&2
      base_url="$local_candidate"
      FU_BASE_URL="$local_candidate"
      export FU_BASE_URL
      return 0
    fi
  done

  printf "Warning: %s/api/mission-computer.json is not valid Future Union JSON, and no local dev server was found.\n" "$base_url" >&2
}

resolve_runner_path() {
  if command -v fucr >/dev/null 2>&1; then
    command -v fucr
    return 0
  fi

  case "$0" in
    /*) printf "%s\n" "$0" ;;
    *) printf "%s/%s\n" "$(pwd)" "$0" ;;
  esac
}

install_fucr_command() {
  if [ "$FUCR_INSTALL" = "0" ]; then
    return 0
  fi

  case "$0" in
    /*) source_path="$0" ;;
    *) source_path="$(pwd)/$0" ;;
  esac

  if [ ! -f "$source_path" ]; then
    return 0
  fi

  mkdir -p "$FUCR_BIN_DIR"
  target="$FUCR_BIN_DIR/fucr"
  if [ ! -f "$target" ] || ! cmp -s "$source_path" "$target"; then
    cp "$source_path" "$target"
    chmod +x "$target"
    printf "Installed Future Union Control Room command: %s\n" "$target" >&2
  fi

  case ":$PATH:" in
    *":$FUCR_BIN_DIR:"*) ;;
    *) printf "Add %s to PATH to run \`fucr\` from anywhere.\n" "$FUCR_BIN_DIR" >&2 ;;
  esac
}

release_manifest_value() {
  key="$1"
  manifest="$2"
  sed -n "s/.*\"$key\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$manifest" | head -n 1
}

update_fucr_command() {
  manifest_url="$base_url/agent-pack/fucr-release.json"
  tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/future-union-update.XXXXXX")
  manifest_file="$tmp_dir/fucr-release.json"
  runner_file="$tmp_dir/fucr"

  printf "Checking FUCR release manifest at %s\n" "$manifest_url" >&2
  curl -fsS "$manifest_url" -o "$manifest_file"
  remote_version=$(release_manifest_value version "$manifest_file")
  runner_path=$(release_manifest_value runnerPath "$manifest_file")
  runner_sha256=$(release_manifest_value runnerSha256 "$manifest_file")

  case "$remote_version" in
    [0-9]*.[0-9]*.[0-9]*) ;;
    *) fail "Invalid FUCR release version in $manifest_url" ;;
  esac
  case "$runner_path" in
    /agent-pack/*) ;;
    *) fail "Invalid FUCR runner path in $manifest_url" ;;
  esac
  if ! printf "%s" "$runner_sha256" | grep -Eq '^[0-9a-f]{64}$'; then
    fail "Invalid FUCR runner checksum in $manifest_url"
  fi

  curl -fsS "$base_url$runner_path" -o "$runner_file"
  printf "%s  %s\n" "$runner_sha256" "$runner_file" | sha256sum -c - >/dev/null

  mkdir -p "$FUCR_BIN_DIR"
  target="$FUCR_BIN_DIR/fucr"
  target_sha256=""
  [ -f "$target" ] && target_sha256=$(sha256sum "$target" | cut -d' ' -f1)
  if [ "$target_sha256" = "$runner_sha256" ]; then
    printf "FUCR is current at %s.\n" "$remote_version" >&2
  else
    staged="$FUCR_BIN_DIR/.fucr.new.$$"
    cp "$runner_file" "$staged"
    chmod 0755 "$staged"
    mv "$staged" "$target"
    printf "Updated FUCR %s -> %s at %s\n" "$FUCR_RELEASE_VERSION" "$remote_version" "$target" >&2
  fi

  rm -rf "$tmp_dir"
  tmp_dir=""
}

run_fucr_doctor() {
  printf "# Future Union Control Room Doctor\n\n"
  printf "Base URL: %s\n" "$base_url"
  printf "Cache: %s\n" "$FUCR_CACHE_DIR"
  printf "Command: %s\n" "$(resolve_runner_path)"
  printf "Profile: %s\n" "$FUCR_PROFILE_FILE"
  if [ -f "$FUCR_PROFILE_FILE" ]; then
    printf "Profile status: saved\n"
    printf "Contributor: %s\n" "${FU_CONTRIBUTOR_NAME:-anonymous or unset}"
    printf "Credit mode: %s\n" "$FU_CREDIT_MODE"
  else
    printf "Profile status: not saved; run \`fucr profile edit\`\n"
  fi
  [ -f "$FUCR_CACHE_DIR/version.txt" ] && printf "Cached version: %s\n" "$(cat "$FUCR_CACHE_DIR/version.txt")"
  [ -f "$FUCR_CACHE_DIR/pi-profile-setup" ] && printf "Pi profile setup: %s\n" "$(cat "$FUCR_CACHE_DIR/pi-profile-setup")"
  [ -f "$FUCR_CACHE_DIR/pi-autoresearch-setup" ] && printf "Pi autoresearch setup (pre-profile marker): %s\n" "$(cat "$FUCR_CACHE_DIR/pi-autoresearch-setup")"
  [ -f "$FUCR_CACHE_DIR/radicle-node-setup" ] && printf "Radicle node onboarding: %s\n" "$(cat "$FUCR_CACHE_DIR/radicle-node-setup")"
  printf "\n"
  for tool in curl cargo git node npm python3 pi codex claude rad rad-experiment; do
    if command -v "$tool" >/dev/null 2>&1; then
      printf "OK   %-7s %s\n" "$tool" "$(command -v "$tool")"
    else
      printf "MISS %-7s\n" "$tool"
    fi
  done
  if command -v pi >/dev/null 2>&1; then
    printf "Pi profile: fu-pi-profile v%s (manifest: %s/agent-pack/pi-profile.json)\n" "$FU_PI_PROFILE_VERSION" "$base_url"
    pi_profile_status
    if pi list 2>/dev/null | grep -Fq "pi-landstrip"; then
      printf "OK   %-7s %s\n" "sandbox" "pi-landstrip installed (Landlock sandbox; profile pin $FU_PI_SANDBOX_PIN)"
    else
      printf "INFO %-7s %s\n" "sandbox" "pi-landstrip not installed; opt in with FU_ENABLE_PI_SANDBOX=1 (pin $FU_PI_SANDBOX_PIN)"
    fi
    if pi list 2>/dev/null | grep -Fq "pi-cc"; then
      printf "OK   %-7s %s\n" "pi-cc" "Community Computer extension installed (can publish to Community Computer)"
    else
      printf "INFO %-7s %s\n" "pi-cc" "not installed; FU publish lane works without it"
    fi
    if pi list 2>/dev/null | grep -Eiq "pi-exa|@feniix/pi-exa|exa"; then
      printf "OK   %-7s %s\n" "pi-exa" "Exa research extension detected"
    else
      printf "INFO %-7s %s\n" "pi-exa" "not installed; optional high-throughput web research adapter"
    fi
  fi
  if [ -n "${EXA_API_KEY:-}" ]; then
    printf "OK   %-7s %s\n" "exa-key" "EXA_API_KEY set for local Exa/Websets-capable tools"
  else
    printf "INFO %-7s %s\n" "exa-key" "not set; Exa/Websets tools will need a local key"
  fi
  # BYOK lane status: key COUNTS only, values are never printed.
  if [ -f "$FU_BYOK_KEYS_FILE" ]; then
    printf "OK   %-7s %s\n" "byok" "$FU_BYOK_KEY_COUNT key name(s) loaded from $FU_BYOK_KEYS_FILE (values never shown or submitted)"
  else
    printf "INFO %-7s %s\n" "byok" "no keys file at $FU_BYOK_KEYS_FILE (free lane; optional, config-file only — the runner never asks for API keys)"
  fi
  if command -v rad >/dev/null 2>&1; then
    rad self >/dev/null 2>&1 && printf "OK   %-7s identity ready\n" "rad" || printf "WARN %-7s run rad auth before publishing\n" "rad"
    if rad node status 2>&1 | grep -Fq "Node is stopped"; then
      printf "WARN %-7s local node stopped; start it in FUCR with n before publishing from this machine\n" "node"
    else
      printf "OK   %-7s local node running or reachable\n" "node"
    fi
  fi
  printf "\nRun \`fucr update\` to refresh the cached Ratatui control room.\n"
  printf "Run \`fucr sync all\` from your mission parent folder to update old workspace helpers without touching packet work.\n"
}

workspace_slug_from_dir() {
  workspace_path="$1"
  if command -v node >/dev/null 2>&1 && [ -f "$workspace_path/context/action.json" ]; then
    node -e '
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
process.stdout.write(payload.action?.slug || "");
' "$workspace_path/context/action.json" 2>/dev/null || true
  fi
}

workspace_slug() {
  workspace_path="$1"
  slug_value="$(workspace_slug_from_dir "$workspace_path")"
  if [ -n "$slug_value" ]; then
    printf "%s\n" "$slug_value"
    return 0
  fi
  basename "$workspace_path" | sed 's/^future-union-mission-//'
}

repair_fetch_file() {
  url="$1"
  destination="$2"
  if [ "$FU_DRY_RUN" = "1" ]; then
    printf "DRY RUN: would refresh %s from %s\n" "$destination" "$url"
    return 0
  fi
  mkdir -p "$(dirname "$destination")"
  printf "Refreshing %s\n" "$destination"
  if curl -fsS "$url" -o "$destination"; then
    return 0
  fi

  printf "WARN: could not refresh %s from %s. Existing local file was left alone if present.\n" "$destination" "$url" >&2
  return 0
}

write_action_context_from_feed() {
  feed_file="$1"
  action_slug="$2"
  destination="$3"
  command -v node >/dev/null 2>&1 || return 1
  node -e '
const fs = require("node:fs");
const [feedPath, slug, destination] = process.argv.slice(1);
const payload = JSON.parse(fs.readFileSync(feedPath, "utf8"));
const action = (payload.actions || []).find((item) => item.slug === slug);
if (!action) process.exit(2);
const missionSlug = action.parentMissionSlug || action.missionSlug;
const mission = (payload.missions || []).find((item) => item.slug === missionSlug) || null;
const body = {
  site: {
    name: payload.missionComputer?.title ? "Future Union" : "Future Union",
    url: payload.missionRunner?.siteUrl || "",
  },
  action,
  mission,
  missionComputer: payload.missionComputer || {},
  missionRunner: payload.missionRunner || {},
  agentPack: payload.agentPack || {},
  repairedFromMissionFeed: true,
};
fs.writeFileSync(destination, JSON.stringify(body, null, 2) + "\n");
' "$feed_file" "$action_slug" "$destination"
}

repair_action_context() {
  action_slug="$1"
  destination="$2"
  action_url="$base_url/api/actions/$action_slug.json"
  if [ "$FU_DRY_RUN" = "1" ]; then
    printf "DRY RUN: would refresh %s from %s\n" "$destination" "$action_url"
    return 0
  fi

  mkdir -p "$(dirname "$destination")"
  printf "Refreshing %s\n" "$destination"
  if curl -fsS "$action_url" -o "$destination"; then
    return 0
  fi

  feed_tmp="$(mktemp "${TMPDIR:-/tmp}/future-union-feed.XXXXXX")"
  if curl -fsS "$base_url/api/mission-computer.json" -o "$feed_tmp" \
    && write_action_context_from_feed "$feed_tmp" "$action_slug" "$destination"; then
    rm -f "$feed_tmp"
    printf "Rebuilt %s from /api/mission-computer.json because %s was unavailable.\n" "$destination" "$action_url"
    return 0
  fi

  rm -f "$feed_tmp"
  printf "WARN: could not rebuild action context for %s. Try FU_BASE_URL=https://www.futureunion.org.uk fucr sync %s\n" "$action_slug" "$destination" >&2
  return 0
}

write_repaired_runner_json() {
  workspace_path="$1"
  action_slug="$2"
  destination="$workspace_path/context/runner.json"
  if [ "$FU_DRY_RUN" = "1" ]; then
    printf "DRY RUN: would refresh %s\n" "$destination"
    return 0
  fi
  mkdir -p "$workspace_path/context"
  {
    printf "{\n"
    printf "  \"baseUrl\": \"%s\",\n" "$base_url"
    printf "  \"actionSlug\": \"%s\",\n" "$action_slug"
    printf "  \"generatedAt\": \"%s\",\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
    printf "  \"runner\": \"fucr sync\"\n"
    printf "}\n"
  } > "$destination"
}

repair_workspace() {
  workspace_path="${1%/}"
  if [ ! -d "$workspace_path" ]; then
    fail "Workspace does not exist: $workspace_path"
  fi
  if [ ! -f "$workspace_path/.future-union-runner" ] && [ ! -f "$workspace_path/context/runner.json" ]; then
    fail "Refusing to sync $workspace_path because it is not a Future Union mission workspace."
  fi

  slug="$(workspace_slug "$workspace_path")"
  if [ -z "$slug" ]; then
    fail "Could not infer action slug for $workspace_path"
  fi

  printf "Syncing %s (%s)\n" "$workspace_path" "$slug"
  printf "Preserving packet, outcome, next-work, review, sources, contribution, credit, share, Radicle, and room notes.\n"
  printf "Refreshing generated boilerplate: README.md, PROMPT.md, SUBMIT.md, helper, and context. PACKET.md is never touched.\n"

  repair_action_context "$slug" "$workspace_path/context/action.json"
  repair_fetch_file "$base_url/api/mission-computer.json" "$workspace_path/context/future-union-context.json"
  repair_fetch_file "$base_url/agent-pack/actions/$slug/mission.json" "$workspace_path/context/mission.json"
  repair_fetch_file "$base_url/agent-pack/context/safety-rules.md" "$workspace_path/context/safety-rules.md"
  repair_fetch_file "$base_url/agent-pack/context/review-policy.md" "$workspace_path/context/review-policy.md"
  repair_fetch_file "$base_url/agent-pack/AGENTS.md" "$workspace_path/AGENTS.md"
  repair_fetch_file "$base_url/agent-pack/RADICLE_NETWORK.md" "$workspace_path/RADICLE_NETWORK.md"
  repair_fetch_file "$base_url/agent-pack/bin/fu-mission" "$workspace_path/bin/fu-mission"
  repair_fetch_file "$base_url/agent-pack/actions/$slug/README.md" "$workspace_path/README.md"
  repair_fetch_file "$base_url/agent-pack/actions/$slug/PROMPT.md" "$workspace_path/PROMPT.md"
  repair_fetch_file "$base_url/agent-pack/actions/$slug/SUBMIT.md" "$workspace_path/SUBMIT.md"
  repair_fetch_file "$base_url/agent-pack/actions/$slug/SKILL.md" "$workspace_path/.agents/skills/future-union/SKILL.md"
  write_repaired_runner_json "$workspace_path" "$slug"

  if [ "$FU_DRY_RUN" != "1" ]; then
    chmod +x "$workspace_path/bin/fu-mission"
    (
      cd "$workspace_path"
      FU_AGENT=auto FU_BASE_URL="$base_url" ./bin/fu-mission experiment-init >/dev/null 2>&1 || true
      FU_AGENT=auto FU_BASE_URL="$base_url" ./bin/fu-mission mark-state synced >/dev/null 2>&1 || true
    )
    printf "Synced %s. Helper version: %s\n" "$workspace_path" "$("$workspace_path/bin/fu-mission" version 2>/dev/null || printf unknown)"
  fi
}

repair_workspaces() {
  target="${1:-all}"
  if [ "$target" = "all" ]; then
    found=0
    for candidate in future-union-mission-*; do
      [ -d "$candidate" ] || continue
      found=1
      repair_workspace "$candidate"
    done
    if [ "$found" = "0" ]; then
      printf "No future-union-mission-* folders found under %s.\n" "$(pwd)"
    fi
    return 0
  fi

  repair_workspace "$target"
}

confirm_install() {
  label="$1"
  command_text="$2"

  if [ "$FU_DRY_RUN" = "1" ]; then
    printf "DRY RUN: would install %s with: %s\n" "$label" "$command_text" >&2
    return 1
  fi

  if [ "$FU_YES" = "1" ]; then
    return 0
  fi

  if [ -t 0 ]; then
    printf "Install %s now with %s? [y/N] " "$label" "$command_text" >&2
    read answer
    case "$answer" in
      y|Y|yes|YES) return 0 ;;
    esac
  fi

  printf "Skipped %s install. Continuing with available local tools.\n" "$label" >&2
  return 1
}

validate_agent() {
  case "$FU_AGENT" in
    py|pyresearch|pi-autoresearch) FU_AGENT="pi" ;;
    custom) [ -n "$FU_AGENT_CMD" ] || fail "FU_AGENT=custom requires FU_AGENT_CMD." ;;
    auto|pi|codex|claude|none) ;;
    *) fail "FU_AGENT must be auto, pi, codex, claude, custom, or none." ;;
  esac
}

validate_start_mode() {
  case "$FU_START" in
    ask|auto|1|yes|true|0|no|false) ;;
    *) fail "FU_START must be ask, auto, 1, or 0." ;;
  esac
}

validate_tui_mode() {
  case "$FU_TUI" in
    no|false) FU_TUI=0 ;;
    1|yes|true) FU_TUI=ratatui ;;
    0|ratatui|python|textual|lite|node) ;;
    *) fail "FU_TUI must be 0 (default, plain shell), ratatui, lite, textual, or node." ;;
  esac
}

ensure_cargo() {
  if command -v cargo >/dev/null 2>&1; then
    return 0
  fi

  if [ "$FU_BOOTSTRAP" = "0" ]; then
    return 1
  fi

  install_command="curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal"
  if confirm_install "Rust toolchain" "$install_command"; then
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal >&2
    PATH="$HOME/.cargo/bin:$PATH"
    export PATH
  fi

  command -v cargo >/dev/null 2>&1
}

pi_pin_name() {
  pin_body="${1#npm:}"
  printf "%s\n" "${pin_body%@*}"
}

pi_pin_version() {
  printf "%s\n" "${1##*@}"
}

pi_installed_version() {
  pin_pkg_json="$HOME/.pi/agent/npm/node_modules/$1/package.json"
  [ -f "$pin_pkg_json" ] || return 1
  sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$pin_pkg_json" | head -n 1
}

# One line per core pin: OK / DRIFT / MISS, doctor-style. Validates the
# installed set against the fu-pi-profile pins (see /agent-pack/pi-profile.json).
pi_profile_status() {
  pi_list_output="$(pi list 2>/dev/null || true)"
  for pin in $FU_PI_CORE_PINS; do
    pin_name="$(pi_pin_name "$pin")"
    pin_want="$(pi_pin_version "$pin")"
    if printf "%s" "$pi_list_output" | grep -Fq "npm:$pin_name"; then
      pin_have="$(pi_installed_version "$pin_name" || printf "unknown")"
      if [ "$pin_have" = "$pin_want" ]; then
        printf "OK   %s %s (fu-pi-profile v%s pin)\n" "$pin_name" "$pin_have" "$FU_PI_PROFILE_VERSION"
      else
        printf "DRIFT %s installed %s, profile pins %s. Fix: pi install %s\n" "$pin_name" "$pin_have" "$pin_want" "$pin"
      fi
    elif printf "%s" "$pi_list_output" | grep -Fq "$pin_name"; then
      printf "DRIFT %s installed from an unpinned source, profile pins %s. Fix: pi install %s (then drop the old entry from ~/.pi/agent/settings.json)\n" "$pin_name" "$pin_want" "$pin"
    else
      printf "MISS %s not installed. Fix: pi install %s\n" "$pin_name" "$pin"
    fi
  done
}

maybe_setup_pi_profile() {
  if [ "${FUCR_SETUP:-1}" = "0" ]; then
    return 0
  fi
  if ! command -v pi >/dev/null 2>&1; then
    return 0
  fi

  marker="$FUCR_CACHE_DIR/pi-profile-setup"
  mkdir -p "$FUCR_CACHE_DIR"
  missing_pins=""
  for pin in $FU_PI_CORE_PINS; do
    # Any installed source counts here to avoid double-installing over an
    # older git/unpinned entry; `fucr doctor` reports pin drift with the fix.
    if ! pi list 2>/dev/null | grep -Fq "$(pi_pin_name "$pin")"; then
      missing_pins="$missing_pins $pin"
    fi
  done
  if [ -z "$missing_pins" ]; then
    printf "installed fu-pi-profile v%s\n" "$FU_PI_PROFILE_VERSION" > "$marker"
    return 0
  fi

  for pin in $missing_pins; do
    if confirm_install "$(pi_pin_name "$pin") (fu-pi-profile v$FU_PI_PROFILE_VERSION pin)" "pi install $pin"; then
      pi install "$pin" >&2 || true
    fi
  done
  printf "setup ran for fu-pi-profile v%s\n" "$FU_PI_PROFILE_VERSION" > "$marker"
}

maybe_setup_pi_sandbox() {
  if [ "${FUCR_SETUP:-1}" = "0" ]; then
    return 0
  fi
  if [ "$FU_ENABLE_PI_SANDBOX" != "1" ]; then
    return 0
  fi
  command -v pi >/dev/null 2>&1 || return 0
  if pi list 2>/dev/null | grep -Fq "pi-landstrip"; then
    return 0
  fi

  printf "FU_ENABLE_PI_SANDBOX=1 set: pi-landstrip adds Landlock sandboxing with interactive permission prompts. Its default policy is strict (network off until you approve domains), so expect approval dialogs during the run.\n" >&2
  if confirm_install "pi-landstrip sandbox (fu-pi-profile v$FU_PI_PROFILE_VERSION pin)" "pi install $FU_PI_SANDBOX_PIN"; then
    pi install "$FU_PI_SANDBOX_PIN" >&2 || true
  fi
}

maybe_update_pi_packages() {
  if [ "${FUCR_SETUP:-1}" = "0" ] || [ "$FU_BOOTSTRAP" = "0" ]; then
    return 0
  fi
  command -v pi >/dev/null 2>&1 || return 0

  marker="$FUCR_CACHE_DIR/pi-update-checked"
  today="$(date -u +%Y-%m-%d 2>/dev/null || printf "today")"
  case "$FU_PI_UPDATE" in
    0|no|false|skip) return 0 ;;
    1|yes|true|auto) answer=yes ;;
    ask) answer="" ;;
    *) fail "FU_PI_UPDATE must be ask, yes, or no." ;;
  esac

  if [ -z "$answer" ]; then
    if [ -f "$marker" ] && grep -Fqx "$today" "$marker" 2>/dev/null; then
      return 0
    fi
    if [ "$FU_YES" = "1" ]; then
      answer=yes
    elif [ -t 0 ]; then
      printf "Update Pi packages before FUCR starts? This runs pi update --extensions and usually clears Pi's package-update banner. [y/N] " >&2
      read answer
    else
      answer=no
    fi
  fi

  mkdir -p "$FUCR_CACHE_DIR"
  printf "%s\n" "$today" > "$marker"
  case "$answer" in
    y|Y|yes|YES|true|1)
      printf "Updating Pi packages. This does not publish anything. fu-pi-profile pins are versioned specs, so Pi skips them here and they stay pinned.\n" >&2
      pi update --extensions >&2 || printf "Pi package update failed; continuing without blocking FUCR.\n" >&2
      ;;
    *)
      printf "Skipped Pi package update. If Pi shows a package banner later, run: pi update --extensions\n" >&2
      ;;
  esac
}

maybe_setup_community_computer_tools() {
  if [ "${FUCR_SETUP:-1}" = "0" ]; then
    return 0
  fi
  if [ "${FU_ENABLE_PI_CC:-0}" != "1" ]; then
    return 0
  fi

  missing=0
  command -v rad-experiment >/dev/null 2>&1 || missing=1
  if command -v pi >/dev/null 2>&1 && ! pi list 2>/dev/null | grep -Fq "pi-cc"; then
    missing=1
  fi
  [ "$missing" = "1" ] || return 0

  printf "FU_ENABLE_PI_CC=1 set: installing the Community Computer bridge. This may add pi-cc, which can publish autoresearch tapes to Community Computer after explicit tool use.\n" >&2
  if confirm_install "Community Computer Radicle/Pi tools" "curl -sSf https://community.computer/install | sh"; then
    curl -sSf https://community.computer/install | sh >&2 || true
  fi
}

rad_node_running() {
  command -v rad >/dev/null 2>&1 || return 1
  status_output="$(rad node status 2>&1 || true)"
  printf "%s" "$status_output" | grep -Fq "Node is stopped" && return 1
  printf "%s" "$status_output" | grep -Eiq "running|node id|listening|connected"
}

maybe_start_radicle_node() {
  if [ "${FUCR_SETUP:-1}" = "0" ] || [ "$FU_BOOTSTRAP" = "0" ]; then
    return 0
  fi
  # Radicle is the optional advanced lane. Skip node onboarding entirely
  # unless the human opted in with FU_RADICLE=1 (or set FU_RADICLE_SETUP).
  if [ "$FU_RADICLE" != "1" ] && [ "$FU_RADICLE_SETUP" = "ask" ]; then
    return 0
  fi
  if ! command -v rad >/dev/null 2>&1; then
    return 0
  fi
  if ! rad self >/dev/null 2>&1; then
    printf "Radicle is installed, but identity is not ready. Run rad auth when you want to publish FU packets.\n" >&2
    return 0
  fi
  if rad_node_running; then
    mkdir -p "$FUCR_CACHE_DIR"
    printf "running\n" > "$FUCR_CACHE_DIR/radicle-node-setup"
    return 0
  fi
  marker="$FUCR_CACHE_DIR/radicle-node-setup"

  case "$FU_RADICLE_SETUP" in
    0|no|false|skip) return 0 ;;
    1|yes|true|auto) answer=yes ;;
    ask) answer="" ;;
    *) fail "FU_RADICLE_SETUP must be ask, yes, or no." ;;
  esac

  if [ -z "$answer" ]; then
    if [ -f "$marker" ]; then
      return 0
    fi
    if [ "$FU_YES" = "1" ]; then
      answer=yes
    elif [ -t 0 ]; then
      printf "Start your local Radicle node now? This enables FU network sync/publish from this machine but publishes nothing by itself. [y/N] " >&2
      read answer
    else
      answer=no
    fi
  fi

  case "$answer" in
    y|Y|yes|YES|true|1)
      mkdir -p "$FUCR_CACHE_DIR"
      printf "accepted\n" > "$marker"
      printf "Starting local Radicle node. Nothing is published by this step.\n" >&2
      rad node start >&2 || printf "Radicle node start failed. You can start it later inside FUCR with n.\n" >&2
      ;;
    *)
      mkdir -p "$FUCR_CACHE_DIR"
      printf "skipped\n" > "$marker"
      printf "Skipped Radicle node start. You can start it later inside FUCR with n.\n" >&2
      ;;
  esac
}

launch_ratatui_tui() {
  if ! ensure_cargo; then
    printf "Rust/cargo is not available, so the Ratatui Control Room cannot start.\n" >&2
    return 1
  fi
  maybe_setup_pi_profile
  maybe_setup_pi_sandbox
  maybe_update_pi_packages
  maybe_setup_community_computer_tools
  maybe_start_radicle_node

  rust_dir="${FU_RATATUI_DIR:-$FUCR_CACHE_DIR/src}"
  binary_dir="$FUCR_CACHE_DIR/bin"
  binary="$binary_dir/fucr-control-room"
  local_version_file="$FUCR_CACHE_DIR/version.txt"
  mkdir -p "$FUCR_WORKSPACE_ROOT"
  workdir="$FUCR_WORKSPACE_ROOT"
  runner_path="$(resolve_runner_path)"
  manifest_url="$base_url/agent-pack/fu-control-room-rs/manifest.txt"
  version_url="$base_url/agent-pack/fu-control-room-rs/version.txt"
  manifest_file="$rust_dir/manifest.txt"
  remote_version="$(curl -fsS "$version_url" 2>/dev/null || printf "dev")"
  remote_version="$(printf "%s" "$remote_version" | tr -d '\r\n')"
  local_version=""
  [ -f "$local_version_file" ] && local_version="$(cat "$local_version_file")"

  if [ "$FUCR_UPDATE" = "1" ] || [ ! -x "$binary" ] || [ "$remote_version" != "$local_version" ]; then
    mkdir -p "$rust_dir" "$binary_dir"
    printf "Building Future Union Control Room %s from %s\n" "$remote_version" "$base_url" >&2
    curl -fsS "$manifest_url" -o "$manifest_file"

    while IFS= read -r file_path; do
      case "$file_path" in
        ""|\#*) continue ;;
      esac
      mkdir -p "$rust_dir/$(dirname "$file_path")"
      curl -fsS "$base_url/agent-pack/fu-control-room-rs/$file_path" -o "$rust_dir/$file_path"
    done < "$manifest_file"

    cargo build --release --manifest-path "$rust_dir/Cargo.toml" >&2
    cp "$rust_dir/target/release/fu-control-room" "$binary"
    chmod +x "$binary"
    printf "%s\n" "$remote_version" > "$local_version_file"
  else
    printf "Opening cached Future Union Control Room %s\n" "$local_version" >&2
  fi

  "$binary" --base "$base_url" --runner "$runner_path" --workdir "$workdir"
}

ensure_textual_tui() {
  tui_python="python3"
  venv_dir="${FU_TUI_VENV:-.future-union/tui-venv}"

  if python3 -c 'import textual, rich' >/dev/null 2>&1; then
    printf "%s\n" "$tui_python"
    return 0
  fi

  if [ -x "$venv_dir/bin/python" ] && "$venv_dir/bin/python" -c 'import textual, rich' >/dev/null 2>&1; then
    printf "%s\n" "$venv_dir/bin/python"
    return 0
  fi

  if [ "$FU_BOOTSTRAP" = "0" ]; then
    return 1
  fi

  install_command="python3 -m venv $venv_dir && $venv_dir/bin/python -m pip install --quiet textual rich"

  if confirm_install "Future Union Textual TUI" "$install_command"; then
    mkdir -p "$(dirname "$venv_dir")"
    if python3 -m venv "$venv_dir" >&2 && "$venv_dir/bin/python" -m pip install --quiet textual rich >&2; then
      printf "%s\n" "$venv_dir/bin/python"
      return 0
    fi
    printf "Textual TUI install failed. Falling back to the lite terminal UI when possible.\n" >&2
  fi

  return 1
}

cleanup() {
  if [ -n "$picker_dir" ] && [ -d "$picker_dir" ]; then
    rm -rf "$picker_dir"
  fi

  if [ -n "$tmp_dir" ] && [ -d "$tmp_dir" ]; then
    rm -rf "$tmp_dir"
  fi
}

launch_tui() {
  picker_dir=$(mktemp -d "${TMPDIR:-/tmp}/future-union-tui.XXXXXX")
  tui_file="$picker_dir/fu-control-room.py"
  lite_tui_file="$picker_dir/fu-control-room-lite.py"
  fallback_tui_file="$picker_dir/fu-control-room.mjs"
  runner_path="$(resolve_runner_path)"
  mkdir -p "$FUCR_WORKSPACE_ROOT"
  cd "$FUCR_WORKSPACE_ROOT"
  printf "Opening Future Union Control Room from %s\n" "$base_url" >&2

  if [ "$FU_TUI" = "ratatui" ] || [ "$FU_TUI" = "1" ] || [ "$FU_TUI" = "yes" ] || [ "$FU_TUI" = "true" ]; then
    if launch_ratatui_tui; then
      exit 0
    fi
    printf "Falling back to the Python terminal Control Room.\n" >&2
    FU_TUI="lite"
  fi

  if [ "$FU_TUI" != "node" ] && command -v python3 >/dev/null 2>&1; then
    curl -fsS "$base_url/agent-pack/fu-control-room.py" -o "$tui_file"
    curl -fsS "$base_url/agent-pack/fu-control-room-lite.py" -o "$lite_tui_file"
    if [ "$FU_TUI" = "lite" ]; then
      FU_BASE_URL="$base_url" FU_RUNNER_PATH="$runner_path" python3 "$lite_tui_file" --base "$base_url" --runner "$runner_path"
      exit "$?"
    fi

    tui_python="$(ensure_textual_tui || true)"
    tui_python="${tui_python:-python3}"
    FU_BASE_URL="$base_url" FU_RUNNER_PATH="$runner_path" "$tui_python" "$tui_file" --base "$base_url" --runner "$runner_path"
    exit "$?"
  fi

  curl -fsS "$base_url/agent-pack/fu-control-room.mjs" -o "$fallback_tui_file"
  FU_BASE_URL="$base_url" FU_RUNNER_PATH="$runner_path" node "$fallback_tui_file" --base "$base_url" --runner "$runner_path"
  exit "$?"
}

pick_action() {
  if ! command -v node >/dev/null 2>&1; then
    fallback_slug="$(resolve_default_slug)"
    printf "Node is not available, so the mission picker is using the default action: %s\n" "$fallback_slug" >&2
    printf "%s\n" "$fallback_slug"
    return 0
  fi

  picker_dir=$(mktemp -d "${TMPDIR:-/tmp}/future-union-picker.XXXXXX")
  actions_file="$picker_dir/actions.json"
  actions_table="$picker_dir/actions.tsv"

  printf "Loading Future Union action board from %s/api/actions.json\n" "$base_url" >&2
  curl -fsS "$base_url/api/actions.json" -o "$actions_file"

  node -e '
const fs = require("node:fs");
const actions = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
actions
  .filter((action) => action.status !== "shipped")
  .forEach((action, index) => {
    const label = [action.columnTitle, action.timeNeeded].filter(Boolean).join(" / ");
    console.log(`${index + 1}\t${action.slug}\t${action.title}\t${label}`);
  });
' "$actions_file" > "$actions_table"

  if [ ! -s "$actions_table" ]; then
    fallback_slug="$(resolve_default_slug)"
    printf "No open actions found, using default action: %s\n" "$fallback_slug" >&2
    printf "%s\n" "$fallback_slug"
    return 0
  fi

  printf "\nFuture Union missions\n" >&2
  awk -F '\t' '{ printf "  %s. %s\n     %s [%s]\n", $1, $3, $2, $4 }' "$actions_table" >&2
  printf "\nChoose a mission [1]: " >&2
  read choice
  choice="${choice:-1}"

  slug_choice=$(awk -F '\t' -v choice="$choice" '$1 == choice { print $2 }' "$actions_table")
  if [ -z "$slug_choice" ]; then
    fail "No mission matched choice: $choice"
  fi

  printf "%s\n" "$slug_choice"
}

resolve_default_slug() {
  priority_tmp=""
  slug_value=""

  if command -v node >/dev/null 2>&1; then
    priority_tmp="$(mktemp "${TMPDIR:-/tmp}/future-union-priority.XXXXXX")"
    if curl -fsS "$base_url/api/priority.json" -o "$priority_tmp" 2>/dev/null; then
      slug_value="$(node - "$priority_tmp" <<'NODE'
const fs = require("node:fs");
const file = process.argv[2];
const payload = JSON.parse(fs.readFileSync(file, "utf8"));
const ranking = Array.isArray(payload.ranking) ? payload.ranking : [];
const firstRanked = ranking
  .slice()
  .sort((left, right) => Number(left.rank || 9999) - Number(right.rank || 9999))
  .find((item) => item && item.slug);
const slug =
  payload.agentDefaultActionSlug ||
  payload.publicDefaultActionSlug ||
  payload.reviewerDefaultActionSlug ||
  (firstRanked && firstRanked.slug) ||
  "";
if (slug) process.stdout.write(slug);
NODE
)" || slug_value=""
    fi
    rm -f "$priority_tmp"
  fi

  if [ -n "$slug_value" ]; then
    printf "%s\n" "$slug_value"
  else
    printf "%s\n" "$DEFAULT_SLUG"
  fi
}

should_start_agent() {
  case "$FU_START" in
    1|yes|true) return 0 ;;
    0|no|false) return 1 ;;
  esac

  if [ "$FU_AGENT" = "none" ]; then
    return 1
  fi

  if [ ! -t 0 ]; then
    return 1
  fi

  printf "\nStart the selected agent in this workspace now? [Y/n] "
  read answer
  case "${answer:-Y}" in
    n|N|no|NO) return 1 ;;
    *) return 0 ;;
  esac
}

base_url=$(trim_trailing_slashes "$FU_BASE_URL")
load_profile
case "$FU_CREDIT_MODE" in
  anonymous) FU_ANONYMOUS=yes ;;
esac
export FU_AGENT FU_AGENT_CMD FU_CONTRIBUTOR_NAME FU_CONTRIBUTOR_HANDLE FU_CONTRIBUTOR_TEAM FU_CONTACT FU_CREDIT_MODE FU_ANONYMOUS FUCR_WORKSPACE_ROOT FUCR_PROFILE_FILE

if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ] || [ "${1:-}" = "help" ]; then
  cat <<EOF
Future Union Control Room

Usage:
  fucr
  fucr version
  fucr control-room
  fucr default
  fucr mission <action-slug>
  fucr profile show|edit|unset|path
  fucr sync [all|/path/to/workspace]
  fucr doctor
  fucr update

Install:
  curl -fsS https://www.futureunion.org.uk/agent-pack/install-fucr.sh | sh

Legacy:
  sh fu-runner.sh [action-slug]

The default path is plain shell: no Rust, no cargo, no Radicle.
No slug opens the plain mission picker in a terminal. \`fucr default\` runs the
current top-priority agent mission from the site priority feed.
Golden path: fucr default (or fucr mission <slug>) -> agent fills PACKET.md ->
./bin/fu-mission finish -> ./bin/fu-mission submit (HTTPS review lane).
The Ratatui Control Room is opt-in: FU_TUI=ratatui or \`fucr control-room\`.
The signed Radicle lane is opt-in: FU_RADICLE=1.

First-run profile:
  fucr asks for local contributor details so reviewers can credit useful work.
  Saved at: $FUCR_PROFILE_FILE
  Nothing in the profile is published automatically. Public credit appears only after human review.

Safety boundary:
  FUCR creates one mission workspace and publish previews include allowlisted mission files.
  The current runner is workspace-disciplined and review-gated, but not a hardened OS sandbox.
  For sensitive experiments, use a separate OS user, container, VM, or wait for FU_SANDBOX mode.

Environment:
  FUCR_BASE_URL=http://127.0.0.1:4392
  FUCR_CONFIG_DIR=$FUCR_CONFIG_DIR
  FUCR_PROFILE_FILE=$FUCR_PROFILE_FILE
  FU_PROFILE_PROMPT=ask|yes|no
  FUCR_CACHE_DIR=$FUCR_CACHE_DIR
  FUCR_WORKSPACE_ROOT=$FUCR_WORKSPACE_ROOT
  FUCR_INSTALL=0
  FU_BASE_URL=http://127.0.0.1:4391
  FU_AGENT=auto|pi|codex|claude|custom|none
  FU_AGENT_CMD="your-agent --safe-mode"
  FU_START=ask|1|0
  FU_PICK=0
  FU_TUI=0|ratatui|lite|textual|node   (default 0: plain shell, no Rust)
  FU_RATATUI_DIR=.future-union/fu-control-room-rs
  FU_TUI_VENV=.future-union/tui-venv
  FU_ENABLE_PI_CC=1
  FU_ENABLE_PI_SANDBOX=1                (opt into the pi-landstrip Landlock sandbox)
  FU_RADICLE=1                          (opt into the signed Radicle lane)
  FU_RADICLE_SETUP=ask|yes|no
  FU_BOOTSTRAP=0
  FU_FORCE=1
  FU_DRY_RUN=1
  FU_CONTRIBUTOR_NAME="Your name"
  FU_CONTRIBUTOR_HANDLE="@handle"
  FU_CONTRIBUTOR_TEAM="Team or group"
  FU_CONTACT="email@example.com"
  FU_CREDIT_MODE=named|team|anonymous|ask-before-public

Optional research accelerators:
  Pi extensions install from the pinned fu-pi-profile manifest (/agent-pack/pi-profile.json); fucr doctor reports version drift.
  EXA_API_KEY=... enables Exa/Websets-capable local tools when installed.
  FU_BYOK_KEYS_FILE=$FU_BYOK_KEYS_FILE
    Optional bring-your-own-key file (NAME=value lines, e.g. EXA_API_KEY=...).
    Loaded silently when present; keys stay on your machine and are never
    printed, never submitted, and never prompted for.
  Pi Exa is optional; install only if you want your local Pi agent to use your Exa account.
  Custom agents are welcome: set FU_AGENT=custom and FU_AGENT_CMD to a command that runs inside the mission workspace.
EOF
  exit 0
fi

if [ "${1:-}" = "-V" ] || [ "${1:-}" = "--version" ] || [ "${1:-}" = "version" ]; then
  printf "%s\n" "$FUCR_RELEASE_VERSION"
  exit 0
fi

case "${1:-}" in
  setup)
    install_fucr_command
    profile_command edit
    exit 0
    ;;
  profile)
    install_fucr_command
    shift
    profile_command "${1:-show}"
    exit 0
    ;;
  update)
    trap cleanup EXIT HUP INT TERM
    install_fucr_command
    update_fucr_command
    if [ "${2:-}" = "--no-launch" ]; then
      exit 0
    fi
    exec "$FUCR_BIN_DIR/fucr" control-room
    ;;
esac

resolve_base_url
trap cleanup EXIT HUP INT TERM
validate_start_mode
validate_tui_mode

case "${1:-}" in
  doctor)
    install_fucr_command
    run_fucr_doctor
    exit 0
    ;;
  sync|repair)
    install_fucr_command
    shift
    repair_workspaces "${1:-all}"
    exit 0
    ;;
  control-room)
    install_fucr_command
    ensure_profile
    # Explicit TUI command: opting in by name overrides the plain-shell default.
    [ "$FU_TUI" = "0" ] && FU_TUI=ratatui
    launch_tui
    ;;
  default|priority|top|run-default)
    install_fucr_command
    ensure_profile
    slug="$(resolve_default_slug)"
    ;;
  mission)
    shift
    ensure_profile
    if [ -n "${1:-}" ]; then
      slug="$1"
    else
      slug="$(resolve_default_slug)"
    fi
    ;;
  "")
    install_fucr_command
    ensure_profile
    if [ "$FU_PICK" != "0" ] && [ -t 0 ]; then
      if [ "$FU_TUI" != "0" ]; then
        launch_tui
      fi
      slug=$(pick_action)
    else
      slug="$(resolve_default_slug)"
    fi
    ;;
  *)
    install_fucr_command
    ensure_profile
    slug="$1"
    ;;
esac

if [ -n "${slug:-}" ]; then
  :
elif [ -n "${1:-}" ]; then
  slug="$1"
elif [ "$FU_PICK" != "0" ] && [ -t 0 ]; then
  if [ "$FU_TUI" != "0" ]; then
    launch_tui
  fi
  slug=$(pick_action)
else
  slug="$(resolve_default_slug)"
fi

validate_agent
maybe_setup_pi_profile
maybe_setup_pi_sandbox
maybe_setup_community_computer_tools
maybe_start_radicle_node

mkdir -p "$FUCR_WORKSPACE_ROOT"
if [ -n "${FU_DIR:-}" ]; then
  workspace="$FU_DIR"
else
  workspace="$FUCR_WORKSPACE_ROOT/future-union-mission-$slug"
fi
marker_file=".future-union-runner"

case "$workspace" in
  ""|"/"|".")
    fail "Refusing to use an unsafe workspace path: $workspace"
    ;;
esac

cat <<EOF
Future Union runner
Action: $slug
Base URL: $base_url
Workspace: $workspace
Agent: $FU_AGENT
Start: $FU_START
Bootstrap: $FU_BOOTSTRAP
Contributor: ${FU_CONTRIBUTOR_NAME:-anonymous or unset}
Team: ${FU_CONTRIBUTOR_TEAM:-none}
Credit mode: $FU_CREDIT_MODE
Idea file: ${FU_IDEA_FILE:-none}
EOF

if [ "$FU_DRY_RUN" = "1" ]; then
  cat <<EOF

DRY RUN: no files or tools will be changed.
Would fetch action context, create the mission workspace, run doctor/status/claim, bootstrap the selected agent when safe, and ask whether to start it.
Would fetch:
  $base_url/api/actions/$slug.json
  $base_url/api/mission-computer.json
  $base_url/agent-pack/actions/$slug/README.md
  $base_url/agent-pack/actions/$slug/MISSION.md
  $base_url/agent-pack/actions/$slug/PROMPT.md
  $base_url/agent-pack/actions/$slug/PACKET.md
  $base_url/agent-pack/actions/$slug/ACTION_PROPOSAL.md
  $base_url/agent-pack/actions/$slug/OUTCOME.md
  $base_url/agent-pack/actions/$slug/NEXT_WORK.md
  $base_url/agent-pack/actions/$slug/REVIEW.md
  $base_url/agent-pack/actions/$slug/SOURCES.csv
  $base_url/agent-pack/actions/$slug/CONTRIBUTION.md
  $base_url/agent-pack/actions/$slug/CREDIT.md
  $base_url/agent-pack/actions/$slug/INTAKE_IDEA.md
  $base_url/agent-pack/actions/$slug/SUBMIT.md
  $base_url/agent-pack/actions/$slug/SKILL.md
Would create:
  RADICLE_NETWORK.md
  OUTCOME.md
  NEXT_WORK.md
  REVIEW.md
  .future-union/rooms/$slug.md
  .git/
  .community-computer/session.json
  EXPERIMENT.md
  autoresearch.md
  autoresearch.sh
EOF
  exit 0
fi

if [ -e "$workspace" ]; then
  if [ ! -f "$workspace/$marker_file" ]; then
    fail "Refusing to overwrite $workspace because it is not marked as a Future Union runner workspace."
  fi

  if [ "$FU_FORCE" = "1" ]; then
    rm -rf "$workspace"
  else
    printf "\nWorkspace already exists. Syncing generated helper/context files and preserving your packet work.\n"
    repair_workspace "$workspace"

    if [ "$FUCR_TUI_CHILD" = "1" ]; then
      cat <<EOF

Workspace ready: $workspace
Use a to resume the agent, u to update generated files again, x to pause, p to preview, or P to publish after preview.
EOF
      exit 0
    fi

    (
      cd "$workspace"
      ./bin/fu-mission status
    )

    if should_start_agent; then
      (
        cd "$workspace"
        FU_AGENT="$FU_AGENT" FU_YES="$FU_YES" ./bin/fu-mission start
      )
    elif [ "$FU_TUI" != "0" ] && [ -t 0 ]; then
      cat <<EOF

Opening the Future Union Control Room with this existing workspace selected.
EOF
      FUCR_SELECT_SLUG="$slug"
      export FUCR_SELECT_SLUG
      launch_tui
    else
      cat <<EOF

Resume this mission:
  cd $workspace
  ./bin/fu-mission status
  ./bin/fu-mission start

After the run:
  ./bin/fu-mission finish
  ./bin/fu-mission submit

Nothing was overwritten.
EOF
    fi
    exit 0
  fi
fi

tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/future-union-runner.XXXXXX")

fetch_file() {
  url="$1"
  destination="$2"
  printf "Fetching %s\n" "$url"
  curl -fsS "$url" -o "$destination"
}

fetch_action_context() {
  action_slug="$1"
  destination="$2"
  action_url="$base_url/api/actions/$action_slug.json"
  printf "Fetching %s\n" "$action_url"
  if curl -fsS "$action_url" -o "$destination"; then
    return 0
  fi

  feed_tmp="$tmp_dir/mission-computer-for-action.json"
  printf "Action endpoint unavailable; rebuilding context from %s/api/mission-computer.json\n" "$base_url" >&2
  fetch_file "$base_url/api/mission-computer.json" "$feed_tmp"
  if write_action_context_from_feed "$feed_tmp" "$action_slug" "$destination"; then
    return 0
  fi

  fail "Action not found in Future Union mission feed: $action_slug"
}

# Validate the action before we create the local workspace.
fetch_action_context "$slug" "$tmp_dir/action.json"

mkdir -p \
  "$tmp_dir/bin" \
  "$tmp_dir/context" \
  "$tmp_dir/.future-union/rooms" \
  "$tmp_dir/.agents/skills/future-union"

printf "Future Union runner workspace for %s\n" "$slug" > "$tmp_dir/$marker_file"
{
  printf "{\n"
  printf "  \"baseUrl\": \"%s\",\n" "$base_url"
  printf "  \"actionSlug\": \"%s\",\n" "$slug"
  printf "  \"generatedAt\": \"%s\",\n" "$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  printf "  \"runner\": \"fucr\"\n"
  printf "}\n"
} > "$tmp_dir/context/runner.json"

mv "$tmp_dir/action.json" "$tmp_dir/context/action.json"
fetch_file "$base_url/api/mission-computer.json" "$tmp_dir/context/future-union-context.json"
fetch_file "$base_url/agent-pack/actions/$slug/mission.json" "$tmp_dir/context/mission.json"
fetch_file "$base_url/agent-pack/context/safety-rules.md" "$tmp_dir/context/safety-rules.md"
fetch_file "$base_url/agent-pack/context/review-policy.md" "$tmp_dir/context/review-policy.md"

fetch_file "$base_url/agent-pack/AGENTS.md" "$tmp_dir/AGENTS.md"
fetch_file "$base_url/agent-pack/RADICLE_NETWORK.md" "$tmp_dir/RADICLE_NETWORK.md"
fetch_file "$base_url/agent-pack/bin/fu-mission" "$tmp_dir/bin/fu-mission"
fetch_file "$base_url/agent-pack/actions/$slug/README.md" "$tmp_dir/README.md"
fetch_file "$base_url/agent-pack/actions/$slug/MISSION.md" "$tmp_dir/MISSION.md"
fetch_file "$base_url/agent-pack/actions/$slug/PROMPT.md" "$tmp_dir/PROMPT.md"
fetch_file "$base_url/agent-pack/actions/$slug/PACKET.md" "$tmp_dir/PACKET.md"
fetch_file "$base_url/agent-pack/actions/$slug/ACTION_PROPOSAL.md" "$tmp_dir/ACTION_PROPOSAL.md"
fetch_file "$base_url/agent-pack/actions/$slug/OUTCOME.md" "$tmp_dir/OUTCOME.md"
fetch_file "$base_url/agent-pack/actions/$slug/NEXT_WORK.md" "$tmp_dir/NEXT_WORK.md"
fetch_file "$base_url/agent-pack/actions/$slug/REVIEW.md" "$tmp_dir/REVIEW.md"
fetch_file "$base_url/agent-pack/actions/$slug/SOURCES.csv" "$tmp_dir/SOURCES.csv"
fetch_file "$base_url/agent-pack/actions/$slug/CONTRIBUTION.md" "$tmp_dir/CONTRIBUTION.md"
fetch_file "$base_url/agent-pack/actions/$slug/CREDIT.md" "$tmp_dir/CREDIT.md"
fetch_file "$base_url/agent-pack/actions/$slug/INTAKE_IDEA.md" "$tmp_dir/INTAKE_IDEA.md"
fetch_file "$base_url/agent-pack/actions/$slug/SUBMIT.md" "$tmp_dir/SUBMIT.md"
fetch_file \
  "$base_url/agent-pack/actions/$slug/SKILL.md" \
  "$tmp_dir/.agents/skills/future-union/SKILL.md"

if [ -n "$FU_IDEA_FILE" ]; then
  if [ ! -f "$FU_IDEA_FILE" ]; then
    fail "FU_IDEA_FILE does not exist: $FU_IDEA_FILE"
  fi

  cp "$FU_IDEA_FILE" "$tmp_dir/context/intake-idea.json"
  {
    printf "\n## Attached Intake File\n\n"
    printf "The runner copied a local idea file into \`context/intake-idea.json\`.\n\n"
    printf "%s\n" "- Source path: $FU_IDEA_FILE"
    printf "%s\n" "- Attached at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  } >> "$tmp_dir/INTAKE_IDEA.md"
fi

{
  printf "\n## Runner Supplied Credit Fields\n\n"
  printf "%s\n" "- Contributor name: $FU_CONTRIBUTOR_NAME"
  printf "%s\n" "- Contributor handle: $FU_CONTRIBUTOR_HANDLE"
  printf "%s\n" "- Contributor team: $FU_CONTRIBUTOR_TEAM"
  printf "%s\n" "- Review contact: $FU_CONTACT"
  printf "%s\n" "- Credit mode: $FU_CREDIT_MODE"
  printf "%s\n" "- Anonymous public credit? $FU_ANONYMOUS"
} >> "$tmp_dir/CREDIT.md"

{
  printf "\n## Runner Metadata\n\n"
  printf "%s\n" "- Generated at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  printf "%s\n" "- Base URL: $base_url"
  printf "%s\n" "- Runner action slug: $slug"
  printf "%s\n" "- Selected agent: $FU_AGENT"
  printf "%s\n" "- Contributor name: $FU_CONTRIBUTOR_NAME"
  printf "%s\n" "- Contributor handle: $FU_CONTRIBUTOR_HANDLE"
  printf "%s\n" "- Contributor team: $FU_CONTRIBUTOR_TEAM"
  printf "%s\n" "- Review contact: $FU_CONTACT"
  printf "%s\n" "- Credit mode: $FU_CREDIT_MODE"
} >> "$tmp_dir/CONTRIBUTION.md"

action_title=""
if command -v node >/dev/null 2>&1; then
  action_title=$(node -e '
const fs = require("node:fs");
const payload = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
process.stdout.write(payload.action?.title || "");
' "$tmp_dir/context/action.json")
fi
action_title="${action_title:-$slug}"
{
  printf "# Action Room: %s\n\n" "$action_title"
  printf "This is the local coordination note for one Future Union action. It is not live chat, and nothing here is sent anywhere by default.\n\n"
  printf "## Status\n\n"
  printf "%s\n" "- Created: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
  printf "%s\n" "- Current blocker:"
  printf "%s\n\n" "- Next handoff:"
  printf "## Sources Found\n\n-\n\n"
  printf "## Claims And Work In Flight\n\n-\n\n"
  printf "## Reviewer Questions\n\n-\n\n"
  printf "## Network Notes\n\n"
  printf "Use this file to prepare a clean public note or Radicle packet. Do not paste secrets, raw transcripts, contact details, or unreviewed personal data here.\n"
} > "$tmp_dir/.future-union/rooms/$slug.md"

chmod +x "$tmp_dir/bin/fu-mission"
mv "$tmp_dir" "$workspace"
tmp_dir=""

(
  cd "$workspace"
  ./bin/fu-mission experiment-init >/dev/null 2>&1 || true
  ./bin/fu-mission mark-state workspace_created empty >/dev/null 2>&1 || true
)

if [ "$FUCR_TUI_CHILD" = "1" ]; then
  cat <<EOF

Created $workspace
Workspace is ready. Use the Control Room keys to start or resume an agent, pause safely, preview a packet, or open the local room.
EOF
  exit 0
fi

(
  cd "$workspace"
  ./bin/fu-mission doctor
  if [ "$FU_BOOTSTRAP" != "0" ]; then
    FU_AGENT="$FU_AGENT" FU_YES="$FU_YES" ./bin/fu-mission bootstrap
  else
    printf "\nBootstrap skipped because FU_BOOTSTRAP=0.\n"
  fi
  printf "\n"
  ./bin/fu-mission status
)

cat <<EOF

Created $workspace
EOF

if should_start_agent; then
  (
    cd "$workspace"
    FU_AGENT="$FU_AGENT" FU_YES="$FU_YES" ./bin/fu-mission start
  )
elif [ "$FU_TUI" != "0" ] && [ -t 0 ]; then
  cat <<EOF

Opening the Future Union Control Room with this workspace ready.
Use w for workspace, a for agent/resume, x to pause, p to preview, and ? for help.
EOF
  FUCR_SELECT_SLUG="$slug"
  export FUCR_SELECT_SLUG
  launch_tui
else
  cat <<EOF

When you are ready:
  cd $workspace
  ./bin/fu-mission room
  ./bin/fu-mission start

If you stop midway:
  ./bin/fu-mission pause
  ./bin/fu-mission start

After the run:
  ./bin/fu-mission finish
  ./bin/fu-mission submit

Optional signed Radicle lane (advanced, FU_RADICLE=1):
  ./bin/fu-mission experiment-preview
  ./bin/fu-mission experiment-doctor
  ./bin/fu-mission experiment-publish
EOF
fi
