#!/usr/bin/env sh
# cerefox-local — the host-side command for the LOCAL (Docker) Cerefox world.
#
# Two kinds of verbs:
#   1. HOST verbs (handled here) — manage the container + the host's MCP config:
#        start | stop | restart | upgrade | uninstall | status | logs | configure-agent
#   2. EVERYTHING ELSE — proxied INTO the container's bundled `cerefox` binary via
#      `docker exec`, sourcing the in-container runtime env (the service_role JWT lives
#      ONLY in the container; it never touches the host). So:
#        cerefox-local search "…"      → in-container `cerefox search "…"`
#        cerefox-local document list   → in-container `cerefox document list`
#        cerefox-local mcp             → in-container `cerefox mcp` (stdio over docker exec -i)
#
# Installed by docker/local/install-local.sh. The cloud/Supabase world keeps using the
# separate npm `cerefox` command — the two never collide.
set -eu

CONTAINER="${CEREFOX_LOCAL_CONTAINER:-cerefox-local}"
CONFIG_DIR="${CEREFOX_LOCAL_CONFIG_DIR:-$HOME/.cerefox/local}"
IMAGE="${CEREFOX_LOCAL_IMAGE:-ghcr.io/fstamatelopoulos/cerefox-local:latest}"
VOLUME="${CEREFOX_LOCAL_VOLUME:-cerefox_local_pgdata}"
ENV_FILE="$CONFIG_DIR/.env"

die() { echo "✗ $*" >&2; exit 1; }
need_docker() { command -v docker >/dev/null 2>&1 || die "docker not found"; }
is_running() { [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null || echo false)" = "true" ]; }
host_port() {
  # The published host port for container :8000 (so we can print the right URL).
  docker inspect -f '{{range $p, $c := .NetworkSettings.Ports}}{{if eq $p "8000/tcp"}}{{(index $c 0).HostPort}}{{end}}{{end}}' "$CONTAINER" 2>/dev/null || true
}
# True if some process is LISTENing on TCP $1 (best-effort; needs lsof → if absent we
# can't probe and just let docker surface a bind error).
port_busy() {
  command -v lsof >/dev/null 2>&1 || return 1
  lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1
}
# Echo the first free port stepping +10 from $1 (capped to avoid an infinite loop).
free_port_from() {
  _p="$1"; _n=0
  while port_busy "$_p"; do _p=$((_p + 10)); _n=$((_n + 1)); [ "$_n" -gt 50 ] && break; done
  echo "$_p"
}
# Persist CEREFOX_LOCAL_PORT=<$1> in $ENV_FILE in place (portable; no `sed -i`).
set_env_port() {
  [ -f "$ENV_FILE" ] || return 0
  _tmp="$ENV_FILE.tmp.$$"
  awk -v p="$1" '/^CEREFOX_LOCAL_PORT=/{print "CEREFOX_LOCAL_PORT=" p; next} {print}' \
    "$ENV_FILE" > "$_tmp" && chmod 600 "$_tmp" && mv "$_tmp" "$ENV_FILE"
}

# Config overrides forwarded from the host .env into the container. Deliberately
# EXCLUDES container-managed vars (SUPABASE_URL/KEY, DATABASE_URL, POSTGREST_UPSTREAM,
# the JWT secret) — those are generated/owned inside the container. The single codebase
# reads these the same way in-container as it does for the cloud CLI.
PASSTHROUGH_VARS="CEREFOX_MIN_SEARCH_SCORE CEREFOX_MAX_RESPONSE_BYTES CEREFOX_MAX_CHUNK_CHARS CEREFOX_MIN_CHUNK_CHARS CEREFOX_VERSION_RETENTION_HOURS CEREFOX_VERSION_CLEANUP_ENABLED CEREFOX_OPENAI_BASE_URL CEREFOX_OPENAI_EMBEDDING_MODEL CEREFOX_OPENAI_EMBEDDING_DIMENSIONS CEREFOX_AUTHOR_NAME CEREFOX_AUTHOR_TYPE CEREFOX_REQUESTOR_NAME"

# Emit `-e VAR=value` for each passthrough var present in $ENV_FILE.
config_env_args() {
  [ -f "$ENV_FILE" ] || return 0
  for v in $PASSTHROUGH_VARS; do
    val=$(grep -E "^${v}=" "$ENV_FILE" | head -1 | sed -E "s/^${v}=//")
    [ -n "$val" ] && printf ' -e %s=%s' "$v" "$val"
  done
}

# Recreate the container from the persisted config (used by `upgrade`/`init`). Reads the
# host port + OPENAI key + any config overrides from $ENV_FILE so they survive a recreate.
recreate() {
  need_docker
  [ -f "$ENV_FILE" ] || die "no config at $ENV_FILE — run install-local.sh first"
  # shellcheck disable=SC1090
  PORT="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_PORT:-8000}")"
  BIND_ADDR="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_BIND:-127.0.0.1}")"
  OPENAI_API_KEY="$(. "$ENV_FILE"; echo "${OPENAI_API_KEY:-}")"
  # Ensure the image is available BEFORE removing the running container, so a failed pull
  # (offline, or a tag that doesn't exist yet) can't leave us with no container at all.
  docker image inspect "$IMAGE" >/dev/null 2>&1 || docker pull "$IMAGE" \
    || die "image '$IMAGE' not present locally and pull failed — container left running"
  docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
  # Our own container's port is now freed, so a busy port here means an EXTERNAL
  # conflict (e.g. cloud `cerefox web` grabbed it). Step +10 to a free one and persist.
  if port_busy "$PORT"; then
    _newport=$(free_port_from "$PORT")
    if [ "$_newport" != "$PORT" ]; then
      echo "ℹ Port $PORT is in use by another process — moving local to $_newport."
      PORT="$_newport"; set_env_port "$PORT"
    fi
  fi
  # shellcheck disable=SC2086
  docker run -d --name "$CONTAINER" -p "$BIND_ADDR:$PORT:8000" \
    --restart unless-stopped \
    -v "$VOLUME:/var/lib/postgresql/data" \
    ${OPENAI_API_KEY:+-e OPENAI_API_KEY=$OPENAI_API_KEY} \
    $(config_env_args) \
    "$IMAGE" >/dev/null
  echo "✓ container (re)started → http://localhost:$PORT/app/"
}

usage() {
  cat <<EOF
cerefox-local — manage + use your local (Docker) Cerefox.

Lifecycle (handled on the host):
  init                  set/update the OpenAI API key (+ port), then (re)start
  start | up            start the stopped container
  stop  | down          stop the container (data persists in the volume)
  restart               restart the container
  upgrade               pull the latest image + recreate (keeps your data + OPENAI key)
  uninstall [--purge]   remove the container (--purge also DELETES the data volume)
  status                show container state + URL
  logs [-f]             show container logs
  configure-agent [--tool X]  wire an MCP client (claude-code default; also claude-desktop|cursor|codex|gemini)

Knowledge base (run inside the container — same as the cloud CLI):
  search · document · project · metadata · audit · config · guides · mcp · …
  e.g.  cerefox-local search "release process"
        cerefox-local document ingest notes.md --project-name personal

Run 'cerefox-local <group> --help' for KB verbs.
EOF
}

cmd="${1:-}"; [ "$#" -gt 0 ] && shift || true

case "$cmd" in
  ""|-h|--help|help)
    usage
    # Append the in-container KB help when the container is up, so a single
    # `--help` shows both worlds. (The container's commander knows only the KB verbs.)
    if [ "$cmd" != "" ] && is_running; then
      echo; echo "--- knowledge-base verbs (from the container) ---"
      docker exec -e CEREFOX_PROG_NAME=cerefox-local "$CONTAINER" \
        sh -c 'set -a; . /run/cerefox-runtime.env 2>/dev/null; set +a; exec cerefox --help' 2>/dev/null || true
    fi
    ;;

  init)
    # Post-install setup: set/update the OpenAI API key (+ optional port), persist to the
    # host config, then recreate the container so it picks up the key. Interactive when a
    # TTY is available; otherwise pass --openai-key / --port. Embeddings (ingest + semantic
    # search) need the key; without it the server still runs (FTS only).
    need_docker
    key=""; port=""
    while [ "$#" -gt 0 ]; do
      case "$1" in
        --openai-key) key="${2:-}"; shift 2 ;;
        --openai-key=*) key="${1#*=}"; shift ;;
        --port) port="${2:-}"; shift 2 ;;
        --port=*) port="${1#*=}"; shift ;;
        *) die "unknown init option: $1 (use --openai-key / --port)" ;;
      esac
    done
    mkdir -p "$CONFIG_DIR"; chmod 700 "$CONFIG_DIR"
    cur_key=""; cur_port="8000"; cur_bind="127.0.0.1"
    if [ -f "$ENV_FILE" ]; then
      # shellcheck disable=SC1090
      cur_key="$(. "$ENV_FILE"; echo "${OPENAI_API_KEY:-}")"
      # shellcheck disable=SC1090
      cur_port="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_PORT:-8000}")"
      # shellcheck disable=SC1090
      cur_bind="$(. "$ENV_FILE"; echo "${CEREFOX_LOCAL_BIND:-127.0.0.1}")"
    fi
    if [ -z "$key" ] && [ -t 0 ]; then
      if [ -n "$cur_key" ]; then
        printf 'OpenAI API key [Enter to keep current …%s]: ' "$(printf '%s' "$cur_key" | tail -c 4)"
      else
        printf 'OpenAI API key (sk-…, Enter to skip): '
      fi
      read -r key || true
    fi
    [ -z "$key" ] && key="$cur_key"     # keep current when left blank
    [ -z "$port" ] && port="$cur_port"
    # Preserve any CEREFOX_* tuning overrides already in the file (we only manage key+port).
    preserved=""
    if [ -f "$ENV_FILE" ]; then
      for v in $PASSTHROUGH_VARS; do
        line=$(grep -E "^${v}=" "$ENV_FILE" | head -1)
        [ -n "$line" ] && preserved="${preserved}${line}
"
      done
    fi
    umask 077
    {
      echo "# Cerefox LOCAL host config. The access token lives in the container, not here;"
      echo "# only the OpenAI key + port + optional CEREFOX_* tuning overrides are stored."
      echo "CEREFOX_LOCAL_PORT=$port"
      echo "CEREFOX_LOCAL_BIND=$cur_bind"
      [ -n "$key" ] && echo "OPENAI_API_KEY=$key"
      [ -n "$preserved" ] && printf '%s' "$preserved"
    } > "$ENV_FILE"
    echo "✓ saved config to $ENV_FILE"
    [ -n "$key" ] || echo "  (no OpenAI key set — ingest + semantic search stay disabled; re-run 'cerefox-local init' to add one)"
    recreate
    ;;

  start|up)
    need_docker
    # The container is stopped (so it isn't holding its port). If the stored port is busy,
    # it's an external conflict — recreate on a free port (docker start can't rebind).
    _sp="$(. "$ENV_FILE" 2>/dev/null; echo "${CEREFOX_LOCAL_PORT:-8000}")"
    if port_busy "$_sp"; then
      echo "ℹ Stored port $_sp is in use by another process — recreating on a free port."
      recreate
    else
      docker start "$CONTAINER" >/dev/null && echo "✓ started → http://localhost:$(host_port)/app/"
    fi
    ;;
  stop|down)     need_docker; docker stop "$CONTAINER"  >/dev/null && echo "✓ stopped (data kept)" ;;
  restart)       need_docker; docker restart "$CONTAINER" >/dev/null && echo "✓ restarted → http://localhost:$(host_port)/app/" ;;
  status)
    need_docker
    if is_running; then echo "● running   $CONTAINER   → http://localhost:$(host_port)/app/";
    elif docker inspect "$CONTAINER" >/dev/null 2>&1; then echo "○ stopped   $CONTAINER   (run: cerefox-local start)";
    else echo "✗ not installed   (run install-local.sh)"; fi
    ;;
  logs)          need_docker; docker logs "$@" "$CONTAINER" ;;

  upgrade)
    need_docker
    echo "Pulling $IMAGE …"; docker pull "$IMAGE"
    recreate
    # Refresh this host script from the new image (atomic rename → safe mid-run).
    dst="$CONFIG_DIR/cerefox-local"; tmp="$CONFIG_DIR/.cerefox-local.new"
    if docker cp "$CONTAINER:/opt/cerefox/cerefox-local" "$tmp" 2>/dev/null; then
      chmod +x "$tmp"; mv "$tmp" "$dst" && echo "✓ refreshed cerefox-local from the new image"
    fi
    ;;

  uninstall)
    need_docker
    purge=false; [ "${1:-}" = "--purge" ] && purge=true
    docker rm -f "$CONTAINER" >/dev/null 2>&1 || true
    echo "✓ container removed"
    if [ "$purge" = true ]; then
      docker volume rm "$VOLUME" >/dev/null 2>&1 || true
      echo "✓ data volume '$VOLUME' DELETED"
    else
      echo "  data volume '$VOLUME' kept (use --purge to delete it)"
    fi
    ;;

  configure-agent)
    # Wire an MCP client to the LOCAL server. The MCP server runs INSIDE the container,
    # launched per session as `cerefox-local mcp` (stdio over docker exec) — so the client
    # never needs the JWT. Default tool: claude-code. Override with --tool.
    need_docker
    self="$(command -v cerefox-local 2>/dev/null || echo "$CONFIG_DIR/cerefox-local")"
    tool="claude-code"
    while [ "$#" -gt 0 ]; do
      case "$1" in
        --tool) tool="${2:-}"; shift 2 ;;
        --tool=*) tool="${1#*=}"; shift ;;
        *) die "unknown configure-agent option: $1 (use --tool <client>)" ;;
      esac
    done
    case "$tool" in
      claude-code)
        # Claude Code's `claude` CLI lives on the HOST, so register here (not in-container).
        command -v claude >/dev/null 2>&1 \
          || die "claude CLI not found. Install Claude Code, or use --tool cursor|codex|gemini|claude-desktop."
        claude mcp add cerefox-local -- "$self" mcp \
          && echo "✓ added MCP server 'cerefox-local' to Claude Code (command: $self mcp)"
        ;;
      cursor|codex|gemini|claude-desktop)
        # Reuse the bundled bin's config writers: run it one-shot in a throwaway container
        # (--entrypoint bypasses s6) with ONLY this client's config dir mounted, so the
        # writer edits the HOST file. The Linux container can't compute a macOS/Windows
        # path, so we compute the target here and pass --config-path.
        case "$tool" in
          cursor)  cfg="$HOME/.cursor/mcp.json" ;;
          codex)   cfg="$HOME/.codex/config.toml" ;;
          gemini)  cfg="$HOME/.gemini/settings.json" ;;
          claude-desktop)
            case "$(uname -s)" in
              Darwin) cfg="$HOME/Library/Application Support/Claude/claude_desktop_config.json" ;;
              *)      cfg="$HOME/.config/Claude/claude_desktop_config.json" ;;
            esac ;;
        esac
        cfgdir=$(dirname "$cfg")
        mkdir -p "$cfgdir"
        if docker run --rm --entrypoint /usr/local/bin/cerefox \
             -e CEREFOX_LOCAL_CMD="$self" \
             -v "$cfgdir:$cfgdir" \
             "$IMAGE" \
             configure-agent --tool "$tool" --local --config-path "$cfg"; then
          echo "✓ configured $tool → $cfg (command: $self mcp). Restart $tool to pick it up."
        else
          echo "✗ auto-config failed. Add this MCP server to $tool manually:"
          echo "    command: $self"
          echo "    args:    [\"mcp\"]"
        fi
        ;;
      *)
        die "unknown --tool '$tool' (claude-code | claude-desktop | cursor | codex | gemini)"
        ;;
    esac
    ;;

  *)
    # Proxy any other verb into the container's bundled cerefox binary.
    need_docker
    is_running || die "container '$CONTAINER' is not running — run: cerefox-local start"
    # -i so stdio (notably `mcp`) passes through; -t only when attached to a TTY.
    tflag=""; [ -t 0 ] && [ -t 1 ] && tflag="-t"
    exec docker exec -i $tflag -e CEREFOX_PROG_NAME=cerefox-local "$CONTAINER" \
      sh -c 'set -a; . /run/cerefox-runtime.env 2>/dev/null; set +a; exec cerefox "$@"' cerefox "$cmd" "$@"
    ;;
esac
