#!/usr/bin/env bash
# ai-memory — thin wrapper that invokes the dockerised ai-memory
# binary with the right mounts so install-* commands can edit your
# real config files, bootstrap can read your real project, and
# data-dir commands hit the same volume your server uses.
#
# Install this to ~/.local/bin/ai-memory (ensure ~/.local/bin is on
# PATH). See README "Quick start" for the one-liner.
#
# Special wrapper-only subcommands (not forwarded to the binary):
#   ai-memory upgrade       Pull the latest image + remind to re-stage hooks.
#
# Env overrides:
#   AI_MEMORY_IMAGE         docker image (default: akitaonrails/ai-memory:latest)
#   AI_MEMORY_DOCKER        docker command (default: docker; e.g. "podman")
#   AI_MEMORY_DATA_VOLUME   named volume mounted at /data (default: ai-memory-data)
#   AI_MEMORY_DATA_DIR      host path to bind-mount at /data instead (rare)
#   AI_MEMORY_SERVER_URL    server URL for thin-client commands; if unset,
#                           the wrapper reaches the host loopback server
#                           started by the README quick start.
#   COPILOT_GITHUB_TOKEN    GitHub token for AI_MEMORY_LLM_PROVIDER=copilot
#   AI_MEMORY_NO_TTY=1      force non-interactive even on a real tty
#   AI_MEMORY_NO_VERSION_CHECK=1  skip the once-per-day update check
set -euo pipefail

IMAGE="${AI_MEMORY_IMAGE:-akitaonrails/ai-memory:latest}"
DOCKER="${AI_MEMORY_DOCKER:-docker}"
DATA_VOLUME="${AI_MEMORY_DATA_VOLUME:-ai-memory-data}"
CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/ai-memory"
VERSION_CHECK_FILE="${CACHE_DIR}/last-version-check"
HOOKS_STAGE_DIR="${HOME}/.local/share/ai-memory/hooks"
WRAPPER_RAW_URL="${AI_MEMORY_WRAPPER_URL:-https://raw.githubusercontent.com/akitaonrails/ai-memory/main/bin/ai-memory}"

# ---- version-check helpers (best-effort; never block the wrapper) --------

local_digest() {
  "${DOCKER}" image inspect --format='{{index .RepoDigests 0}}' "${IMAGE}" 2>/dev/null \
    | sed 's/.*@//' || true
}

remote_digest() {
  # docker manifest inspect was experimental pre-20.10; on modern
  # docker it's stable. Silent on failure (offline, podman, etc.).
  "${DOCKER}" manifest inspect "${IMAGE}" 2>/dev/null \
    | grep -oE 'sha256:[a-f0-9]{64}' | head -n 1 || true
}

maybe_warn_outdated() {
  [ -z "${AI_MEMORY_NO_VERSION_CHECK:-}" ] || return 0
  # Skip in non-interactive contexts so we don't pollute pipes / CI.
  [ -t 2 ] || return 0
  # Skip if we checked within the last 24h (works on both BSD + GNU find).
  if [ -f "${VERSION_CHECK_FILE}" ] \
     && [ -z "$(find "${VERSION_CHECK_FILE}" -mtime +0 2>/dev/null)" ]; then
    return 0
  fi
  mkdir -p "${CACHE_DIR}"
  touch "${VERSION_CHECK_FILE}"
  local local_d remote_d
  local_d=$(local_digest)
  remote_d=$(remote_digest)
  [ -n "${local_d}" ] && [ -n "${remote_d}" ] || return 0
  if [ "${local_d}" != "${remote_d}" ]; then
    printf '\033[33mai-memory: a newer image is available on Docker Hub.\033[0m\n' >&2
    printf '           run `ai-memory upgrade` to pull it + refresh hooks.\n' >&2
  fi
}

# ---- upgrade subcommand --------------------------------------------------

self_upgrade_script() {
  command -v curl >/dev/null 2>&1 || { echo "  curl not found; skipping wrapper self-upgrade" >&2; return 0; }
  # Portable script-path resolution (works without GNU readlink/realpath).
  local script_dir script_path tmp
  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  script_path="${script_dir}/$(basename "${BASH_SOURCE[0]}")"
  echo "→ checking for wrapper script updates (${script_path})"
  tmp="$(mktemp)" || return 0
  if ! curl -fsSL "${WRAPPER_RAW_URL}" -o "${tmp}" 2>/dev/null; then
    echo "  could not fetch ${WRAPPER_RAW_URL}; skipping self-upgrade"
    rm -f "${tmp}"
    return 0
  fi
  # Sanity: must look like our bash script. Refuse to install
  # anything that doesn't start with the expected shebang.
  if ! head -n1 "${tmp}" | grep -q '^#!/usr/bin/env bash'; then
    echo "  downloaded file doesn't look like the ai-memory wrapper; skipping"
    rm -f "${tmp}"
    return 0
  fi
  if cmp -s "${tmp}" "${script_path}"; then
    echo "  wrapper already up to date"
    rm -f "${tmp}"
    return 0
  fi
  chmod +x "${tmp}"
  if ! mv "${tmp}" "${script_path}" 2>/dev/null; then
    echo "  could not replace ${script_path} (permission denied?)"
    echo "  rerun with: sudo install -m 0755 ${tmp} ${script_path}"
    return 0
  fi
  echo "  ✓ wrapper updated — re-executing with the new version"
  AI_MEMORY_SKIP_SELF_UPGRADE=1 exec "${script_path}" upgrade
}

cmd_upgrade() {
  if [ -z "${AI_MEMORY_SKIP_SELF_UPGRADE:-}" ]; then
    self_upgrade_script
  fi
  echo "→ pulling ${IMAGE}"
  "${DOCKER}" pull "${IMAGE}"

  local found_agents=()
  if [ -d "${HOOKS_STAGE_DIR}" ]; then
    for d in "${HOOKS_STAGE_DIR}"/*/; do
      [ -d "${d}" ] || continue
      name="$(basename "${d}")"
      # `lib` (and any `_`-prefixed dir) holds shared hook helpers sourced
      # by the per-agent scripts — it is NOT an agent. Skip it so we don't
      # run `install-hooks --agent lib`, which clap rejects. (issue #38)
      case "${name}" in
        lib | _*) continue ;;
      esac
      found_agents+=("${name}")
    done
  fi

  echo
  if [ "${#found_agents[@]}" -gt 0 ]; then
    echo "→ refreshing staged hook scripts for: ${found_agents[*]}"
    for agent in "${found_agents[@]}"; do
      # `install-hooks --apply` re-stages scripts AND idempotently
      # rewrites the agent's settings.json entry. Safe to re-run:
      # the seven hook keys we own get replaced; everything else
      # (other hooks the user wired up) survives untouched.
      echo "    ai-memory install-hooks --agent ${agent} --apply"
      echo "      (uses AI_MEMORY_SERVER_URL/AI_MEMORY_AUTH_TOKEN or the existing ai-memory MCP entry when present)"
      AI_MEMORY_NO_VERSION_CHECK=1 \
      "$0" install-hooks --agent "${agent}" --apply \
        || echo "      (skipped — re-run with the same --server-url / --auth-token used originally)"
    done
  else
    echo "→ no staged hook scripts found at ${HOOKS_STAGE_DIR}"
    echo "  (nothing to refresh — install-hooks hasn't been run with --apply yet)"
  fi

  echo
  if "${DOCKER}" ps --filter "name=^ai-memory$" --format '{{.Names}}' 2>/dev/null \
       | grep -q '^ai-memory$'; then
    # A LOCAL ai-memory container is running. We've just pulled a new
    # image; `docker restart` won't recreate from the new image, so we
    # need to stop+remove+recreate. The safe way is via `docker compose
    # up -d` when a compose file is reachable (it remembers all the
    # ports/volumes/env). Without compose we can't recreate safely
    # because we don't know the original `docker run` args — fall
    # back to a clear instruction.
    local compose_dir=""
    if [ -f "$(pwd)/docker/docker-compose.yml" ]; then
      compose_dir="$(pwd)/docker"
    elif [ -f "$(pwd)/docker-compose.yml" ]; then
      compose_dir="$(pwd)"
    elif [ -f "${HOME}/deploy/ai-memory/docker-compose.yml" ]; then
      compose_dir="${HOME}/deploy/ai-memory"
    fi
    if [ -n "${compose_dir}" ]; then
      echo "→ restarting local ai-memory container via docker compose (${compose_dir})"
      ( cd "${compose_dir}" && "${DOCKER}" compose up -d ) \
        || echo "  (compose restart failed; re-run manually: cd ${compose_dir} && docker compose up -d)"
    else
      echo "→ a local ai-memory container is running but no compose file"
      echo "  was found in \$PWD/docker-compose.yml, ./docker/docker-compose.yml,"
      echo "  or ~/deploy/ai-memory/docker-compose.yml. Restart it manually so"
      echo "  the new image takes effect:"
      echo "      docker stop ai-memory && docker rm ai-memory"
      echo "      # then re-run your docker-run command from the README Quick start"
    fi
  fi

  # If the server runs on a different host (homelab scenario), only
  # the local wrapper + image + hook scripts got refreshed here.
  # Surface that, because the user still needs to deploy on the
  # remote box for the server-side binary to update.
  if [ -n "${AI_MEMORY_SERVER_URL:-}" ] \
     && ! echo "${AI_MEMORY_SERVER_URL}" | grep -qE '^https?://(127\.|localhost|\[?::1\]?)'; then
    echo
    echo "→ Note: AI_MEMORY_SERVER_URL points at ${AI_MEMORY_SERVER_URL}"
    echo "  Your local image is now up to date, but the remote server still"
    echo "  runs the previous version. Redeploy on that host (e.g. \`bin/deploy\`"
    echo "  or \`docker compose pull && docker compose up -d\` in its deploy dir)"
    echo "  for the server-side binary to update."
  fi

  mkdir -p "${CACHE_DIR}"
  touch "${VERSION_CHECK_FILE}"
}

# ---- intercept wrapper-only subcommands ----------------------------------

case "${1:-}" in
  upgrade)
    shift
    cmd_upgrade
    exit 0
    ;;
esac

# ---- normal pass-through to the binary inside docker ---------------------

maybe_warn_outdated || true

TTY_ARGS=()
if [ -z "${AI_MEMORY_NO_TTY:-}" ] && [ -t 0 ] && [ -t 1 ]; then
  TTY_ARGS=(-it)
fi

ENV_ARGS=()
for var in \
  AI_MEMORY_SERVER_URL \
  AI_MEMORY_AUTH_TOKEN \
  AI_MEMORY_LLM_PROVIDER \
  AI_MEMORY_LLM_MODEL \
  AI_MEMORY_LLM_BASE_URL \
  AI_MEMORY_COPILOT_CLIENT_ID \
  AI_MEMORY_EMBEDDING_PROVIDER \
  AI_MEMORY_EMBEDDING_MODEL \
  AI_MEMORY_EMBEDDING_BASE_URL \
  AI_MEMORY_EMBEDDING_DIM \
  AI_MEMORY_ALLOWED_HOSTS \
  AI_MEMORY_HOOK_PLATFORM \
  AI_MEMORY_HOOKS_HOST_ROOT \
  ANTHROPIC_API_KEY \
  OPENAI_API_KEY \
  COPILOT_GITHUB_TOKEN \
  GITHUB_COPILOT_API_TOKEN \
  COPILOT_API_URL \
  VOYAGE_API_KEY \
  LLM_API_KEY \
  RUST_LOG
do
  if [ -n "${!var:-}" ]; then
    ENV_ARGS+=(-e "${var}")
  fi
done

# The wrapper itself runs the CLI inside a short-lived helper container, while
# the README server runs in the long-lived ai-memory container and publishes
# 127.0.0.1:49374 on the host. Inside a normal bridge-network helper,
# 127.0.0.1 would mean "this helper container", so thin-client commands like
# `status` and `bootstrap` could not reach the default server.
NETWORK_ARGS=()
# Default: map the container process to the host user so files written
# through bind mounts (~/.claude/settings.json, $PWD/, …) stay editable
# by the invoking user. macOS is the one exception (see Darwin arm).
USER_ARGS=(-u "$(id -u):$(id -g)")
case "$(uname -s 2>/dev/null || true)" in
  Linux)
    if [ -z "${AI_MEMORY_SERVER_URL:-}" ]; then
      NETWORK_ARGS=(--network host)
    fi
    ;;
  Darwin)
    if [ -z "${AI_MEMORY_SERVER_URL:-}" ]; then
      ENV_ARGS+=(-e "AI_MEMORY_SERVER_URL=http://host.docker.internal:49374")
    fi
    # On macOS, Docker Desktop handles file-sharing permissions via its
    # gRPC/SSH layer.  Passing -u <host-uid>:<host-gid> causes a UID
    # mismatch: the data volume is typically owned by the container's
    # internal uid 1000 (the ai-memory user), but the host UID on macOS
    # is usually 501/502.  The one-shot wrapper container then cannot
    # create log files or write to the data dir, crashing with
    # "Permission denied" in the rolling file appender.
    # Omitting -u lets the container run as its default (uid 1000)
    # which matches the volume owner.
    USER_ARGS=()
    ;;
esac

# Mount the data dir at /data so commands that open the store
# (status, bootstrap, search, write-page, lint, embed, …) see the
# same content the server sees. Bind-mount a host path if the user
# overrode AI_MEMORY_DATA_DIR; otherwise use the named volume.
DATA_ARGS=(-e "AI_MEMORY_DATA_DIR=/data")
if [ -n "${AI_MEMORY_DATA_DIR:-}" ] && [ -d "${AI_MEMORY_DATA_DIR}" ]; then
  DATA_ARGS+=(-v "${AI_MEMORY_DATA_DIR}:/data")
else
  DATA_ARGS+=(-v "${DATA_VOLUME}:/data")
fi

# Mount $HOME at the same path inside the container so:
#   - dirs::home_dir() resolves identically (~/.claude/settings.json
#     auto-detect works without --config-file)
#   - install-hooks stages scripts into ~/.local/share/ai-memory/hooks/
#
# Mount $PWD at /work and set the container workdir there. This
# matters for bootstrap, which needs to find a `.git` at the CWD.
# We *don't* use `-w "$PWD"` because rootless docker / runc errors
# with "mkdir <path>: file exists" when the workdir sits beneath
# an already-bind-mounted parent (the host's $HOME mount). The
# /work path is fresh inside the container so the bind mount
# resolves cleanly. Commands that don't care about the repo CWD
# (install-mcp, install-hooks, status, search, …) just ignore it.
#
# Because the container sees `/work` (not the host's actual path),
# `basename(cwd)` would resolve to "work" for every invocation if the
# CLI used it directly. We pass `AI_MEMORY_HOST_CWD=$PWD` so the
# binary can derive the *real* host-side project name from it. The
# `commands::resolve_project_name` helper checks this env var first
# and falls back to the cwd basename only if it's unset.
exec "${DOCKER}" run --rm ${TTY_ARGS[@]+"${TTY_ARGS[@]}"} \
  ${NETWORK_ARGS[@]+"${NETWORK_ARGS[@]}"} \
  -v "${HOME}:${HOME}" \
  -v "${PWD}:/work" \
  -w /work \
  -e HOME="${HOME}" \
  -e AI_MEMORY_HOST_CWD="${PWD}" \
  ${USER_ARGS[@]+"${USER_ARGS[@]}"} \
  "${DATA_ARGS[@]}" \
  ${ENV_ARGS[@]+"${ENV_ARGS[@]}"} \
  "${IMAGE}" "$@"
