#!/usr/bin/env bash
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${ROOT}/.env"

load_env_file() {
  [[ -f "${ENV_FILE}" ]] || return 0

  local line key value
  while IFS= read -r line || [[ -n "${line}" ]]; do
    line="${line%$'\r'}"
    [[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue
    [[ "${line}" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || fail "unsupported .env line: ${line}"
    key="${BASH_REMATCH[1]}"
    value="${BASH_REMATCH[2]}"
    [[ "${key}" == CONTEXT_KIT_* ]] || fail ".env may only set CONTEXT_KIT_* variables: ${key}"
    [[ "${!key+x}" == "x" ]] && continue
    if [[ "${value}" == \"*\" && "${value}" == *\" ]]; then
      value="${value:1:${#value}-2}"
    elif [[ "${value}" == \'*\' && "${value}" == *\' ]]; then
      value="${value:1:${#value}-2}"
    fi
    export "${key}=${value}"
  done < "${ENV_FILE}"
}

fail() {
  printf 'context-kit: %s\n' "$*" >&2
  exit 1
}

load_env_file

PROJECT="${CONTEXT_KIT_COMPOSE_PROJECT:-context-kit}"
COMPOSE_FILE="${ROOT}/compose.yml"
DATA_DIR="${CONTEXT_KIT_DATA_DIR:-${HOME}/.local/share/context-kit}"
NETWORK="${CONTEXT_KIT_DOCKER_NETWORK:-${PROJECT}_default}"
SEARXNG_PORT="${CONTEXT_KIT_SEARXNG_PORT:-8099}"
DOCS_PORT="${CONTEXT_KIT_DOCS_PORT:-8776}"
DOCS_HTTP_URL="${CONTEXT_KIT_DOCS_HTTP_URL:-http://127.0.0.1:${DOCS_PORT}/mcp}"
DOCS_CONTAINER_NAME="context-kit-docs-mcp"
DOCS_SOURCES_FILE="${DATA_DIR}/docs-sources.txt"
DOCS_DATA_DIR="${DATA_DIR}/docs"
MODELS_DATA_DIR="${DATA_DIR}/models"

WEB_SEARCH_IMAGE="${CONTEXT_KIT_WEB_SEARCH_IMAGE:-context-kit/web-search-mcp:latest}"
DOCS_IMAGE="${CONTEXT_KIT_DOCS_IMAGE:-context-kit/docs-mcp:latest}"
REPOMIX_IMAGE="${CONTEXT_KIT_REPOMIX_IMAGE:-ghcr.io/yamadashy/repomix@sha256:62fb288a3f031f99bc332b73c22acb9ff1cf2a5d8ef2f0196185d5926d9edb2a}"

usage() {
  cat <<'USAGE'
context-kit: local context tools for coding agents

Usage:
  context-kit start                 Start SearXNG + the long-lived docs-mcp service
  context-kit stop                  Stop SearXNG + docs-mcp
  context-kit restart               Restart SearXNG + docs-mcp
  context-kit build                 Build MCP images
  context-kit status                Show services, images, sources, and the docs HTTP endpoint
  context-kit doctor                Check Docker, services, images, sources, and HTTP endpoints
  context-kit redaction-check       Scan this repo for local paths and secret patterns

MCP server commands:
  context-kit web-search            Per-call SearXNG-backed web-search MCP (stdio)
  context-kit docs                  Stdio bridge to the long-lived docs-mcp service
                                    (clients that speak HTTP MCP should connect
                                    directly to the URL printed by `status`)
  context-kit repomix               Per-call Repomix MCP for the current project (stdio)

Assistant snippets:
  context-kit install claude        Print a project .mcp.json snippet using context-kit on PATH
  context-kit install opencode      Print an opencode.json MCP snippet using context-kit on PATH

Configuration is via .env or environment variables. See .env.example.
USAGE
}

compose() {
  CONTEXT_KIT_DATA_DIR="${DATA_DIR}" \
  CONTEXT_KIT_SEARXNG_PORT="${SEARXNG_PORT}" \
  CONTEXT_KIT_DOCS_PORT="${DOCS_PORT}" \
  CONTEXT_KIT_DOCS_UID="$(id -u)" \
  CONTEXT_KIT_DOCS_GID="$(id -g)" \
  CONTEXT_KIT_DOCS_TTL="${CONTEXT_KIT_DOCS_TTL:-24h}" \
  CONTEXT_KIT_DOCS_MAX_GET_BYTES="${CONTEXT_KIT_DOCS_MAX_GET_BYTES:-75000}" \
  CONTEXT_KIT_DOCS_EMBED_MODEL="${CONTEXT_KIT_DOCS_EMBED_MODEL:-BAAI/bge-small-en-v1.5}" \
  CONTEXT_KIT_DOCS_PREINDEX="${CONTEXT_KIT_DOCS_PREINDEX:-0}" \
  BUILDX_BUILDER="${CONTEXT_KIT_BUILDX_BUILDER:-${BUILDX_BUILDER:-default}}" \
  docker compose -p "${PROJECT}" -f "${COMPOSE_FILE}" "$@"
}

write_docs_sources_file() {
  mkdir -p "$(dirname "${DOCS_SOURCES_FILE}")"
  local tmp="${DOCS_SOURCES_FILE}.tmp.$$"
  {
    printf '# generated by context-kit start; edit your CONTEXT_KIT_DOCS_SOURCES file(s) instead\n'
    resolved_sources
  } > "${tmp}"
  mv "${tmp}" "${DOCS_SOURCES_FILE}"
}

ensure_writable_dir() {
  local dir="$1"
  mkdir -p "${dir}"
  if [[ ! -w "${dir}" || ! -x "${dir}" ]]; then
    fail "data directory is not writable by uid $(id -u): ${dir}; fix ownership or set CONTEXT_KIT_DATA_DIR"
  fi
}

prepare_data_dirs() {
  ensure_writable_dir "${DATA_DIR}"
  ensure_writable_dir "${DOCS_DATA_DIR}"
  ensure_writable_dir "${MODELS_DATA_DIR}"
}

check_data_dirs() {
  local ok=0 dir
  for dir in "${DATA_DIR}" "${DOCS_DATA_DIR}" "${MODELS_DATA_DIR}"; do
    if [[ ! -d "${dir}" ]]; then
      printf 'warn data directory missing: %s (run context-kit start)\n' "${dir}"
    elif [[ -w "${dir}" && -x "${dir}" ]]; then
      printf 'pass data directory writable: %s\n' "${dir}"
    else
      printf 'fail data directory not writable by uid %s: %s\n' "$(id -u)" "${dir}"
      ok=1
    fi
  done
  return "${ok}"
}

warn() {
  printf 'warn: %s\n' "$*" >&2
}

json_escape() {
  local s="$1"
  s="${s//\\/\\\\}"
  s="${s//\"/\\\"}"
  s="${s//$'\n'/\\n}"
  s="${s//$'\r'/\\r}"
  s="${s//$'\t'/\\t}"
  printf '%s' "${s}"
}

require_docker() {
  command -v docker >/dev/null 2>&1 || fail "Docker is required"
  docker info >/dev/null 2>&1 || fail "Docker is not running or not reachable"
}

require_image() {
  local image="$1"
  local hint="$2"
  docker image inspect "${image}" >/dev/null 2>&1 || fail "missing image ${image}; run: ${hint}"
}

require_network() {
  docker network inspect "${NETWORK}" >/dev/null 2>&1 || fail "missing Docker network ${NETWORK}; run: context-kit start"
}

wait_for_searxng() {
  command -v curl >/dev/null 2>&1 || return 0

  local attempt
  for attempt in {1..30}; do
    if curl -fsS "http://127.0.0.1:${SEARXNG_PORT}/healthz" >/dev/null 2>&1; then
      return 0
    fi
    sleep 1
  done

  warn "SearXNG did not become ready on 127.0.0.1:${SEARXNG_PORT} after 30s"
}

wait_for_docs_mcp() {
  command -v curl >/dev/null 2>&1 || return 0

  # First-run can take a while: model download + full preindex of every source.
  local attempt
  for attempt in {1..180}; do
    if curl -fsS -o /dev/null "http://127.0.0.1:${DOCS_PORT}/status" 2>/dev/null; then
      return 0
    fi
    sleep 1
  done

  warn "docs-mcp did not become ready on 127.0.0.1:${DOCS_PORT} after 180s (check: docker logs ${DOCS_CONTAINER_NAME})"
}

abs_dir() {
  local path="$1"
  mkdir -p "${path}"
  (cd "${path}" && pwd -P)
}

project_dir() {
  local dir="${CONTEXT_KIT_PROJECT_DIR:-${CLAUDE_PROJECT_DIR:-${PWD}}}"
  (cd "${dir}" && pwd -P)
}

source_files() {
  local configured="${CONTEXT_KIT_DOCS_SOURCES:-config/sources.default.txt}"
  local file
  for file in ${configured}; do
    if [[ "${file}" = /* ]]; then
      printf '%s\n' "${file}"
    else
      printf '%s\n' "${ROOT}/${file}"
    fi
  done
}

resolved_sources() {
  local file line
  while IFS= read -r file; do
    [[ -f "${file}" ]] || fail "docs source file not found: ${file}"
    while IFS= read -r line; do
      line="${line%%#*}"
      line="${line//[$'\t\r\n ']/}"
      [[ -z "${line}" ]] && continue
      printf '%s\n' "${line}"
    done < "${file}"
  done < <(source_files)
}

cmd_build() {
  [[ "$#" -eq 0 ]] || fail "usage: context-kit build"
  require_docker
  # web-search-mcp is still profile-gated (built but not auto-started);
  # docs-mcp is a regular long-lived service so it builds without a profile.
  compose --profile mcp build web-search-mcp
  compose build docs-mcp
  docker pull "${REPOMIX_IMAGE}"
}

cmd_start() {
  require_docker
  prepare_data_dirs
  if ! docker image inspect "${WEB_SEARCH_IMAGE}" >/dev/null 2>&1 || ! docker image inspect "${DOCS_IMAGE}" >/dev/null 2>&1; then
    cmd_build
  fi
  write_docs_sources_file
  compose up -d searxng docs-mcp
  wait_for_searxng
  wait_for_docs_mcp
}

cmd_stop() {
  require_docker
  compose stop searxng docs-mcp
}

cmd_status() {
  require_docker
  printf 'Services\n'
  compose ps
  printf '\nImages\n'
  docker image ls --format '{{.Repository}}:{{.Tag}}\t{{.Size}}' \
    | grep -E '^(context-kit/|ghcr.io/yamadashy/repomix:)' || true
  printf '\nDocs MCP endpoint\n- %s (container: %s)\n' "${DOCS_HTTP_URL}" "${DOCS_CONTAINER_NAME}"
  printf '\nDocs sources\n'
  resolved_sources | sed 's/^/- /'
  printf '\nData directory\n- %s\n' "${DATA_DIR}"
}

cmd_doctor() {
  local ok=0
  printf 'Context Kit doctor\n'

  if command -v docker >/dev/null 2>&1; then
    printf 'pass docker command found\n'
  else
    printf 'fail docker command not found\n'; ok=1
  fi

  if docker info >/dev/null 2>&1; then
    printf 'pass docker daemon reachable\n'
  else
    printf 'fail docker daemon not reachable\n'; ok=1
  fi

  if docker compose version >/dev/null 2>&1; then
    printf 'pass docker compose available\n'
  else
    printf 'fail docker compose unavailable\n'; ok=1
  fi

  if ! check_data_dirs; then
    ok=1
  fi

  if docker network inspect "${NETWORK}" >/dev/null 2>&1; then
    printf 'pass docker network exists: %s\n' "${NETWORK}"
  else
    printf 'warn docker network missing: %s (run context-kit start)\n' "${NETWORK}"
  fi

  for image in "${WEB_SEARCH_IMAGE}" "${DOCS_IMAGE}" "${REPOMIX_IMAGE}"; do
    if docker image inspect "${image}" >/dev/null 2>&1; then
      printf 'pass image exists: %s\n' "${image}"
    else
      printf 'warn image missing: %s\n' "${image}"
    fi
  done

  if command -v curl >/dev/null 2>&1 && curl -fsS "http://127.0.0.1:${SEARXNG_PORT}/healthz" >/dev/null 2>&1; then
    printf 'pass SearXNG responds on 127.0.0.1:%s\n' "${SEARXNG_PORT}"
  else
    printf 'warn SearXNG not responding on 127.0.0.1:%s\n' "${SEARXNG_PORT}"
  fi

  if command -v curl >/dev/null 2>&1 && curl -fsS -o /dev/null "http://127.0.0.1:${DOCS_PORT}/status" 2>/dev/null; then
    printf 'pass docs-mcp HTTP responds on 127.0.0.1:%s\n' "${DOCS_PORT}"
  else
    printf 'warn docs-mcp HTTP not responding on 127.0.0.1:%s (run context-kit start)\n' "${DOCS_PORT}"
  fi

  if [[ "$(resolved_sources | wc -l | tr -d ' ')" -gt 0 ]]; then
    printf 'pass docs sources resolve\n'
  else
    printf 'fail no docs sources configured\n'; ok=1
  fi

  return "${ok}"
}

cmd_web_search() {
  require_docker
  require_network
  require_image "${WEB_SEARCH_IMAGE}" "context-kit build"
  exec docker run --rm -i \
    --label dev.context-kit=true \
    --network "${NETWORK}" \
    -e DEFAULT_SEARCH_PROVIDER="${DEFAULT_SEARCH_PROVIDER:-searxng}" \
    -e SEARXNG_URL="${SEARXNG_URL:-http://searxng:8080}" \
    -e CHROME_PATH="${CHROME_PATH:-/usr/bin/chromium}" \
    -e HTTP_TIMEOUT="${HTTP_TIMEOUT:-15000}" \
    -e MAX_RESULTS="${MAX_RESULTS:-10}" \
    "${WEB_SEARCH_IMAGE}"
}

cmd_docs() {
  # Prefer the `type: remote` MCP config pointing at ${DOCS_HTTP_URL}.
  # This stdio entrypoint is kept for clients that cannot speak HTTP MCP:
  # it spawns a thin mcp-proxy bridge per call but all calls multiplex onto
  # the single long-lived docs-mcp container over the Context Kit Docker
  # network (no Chroma write contention, no host networking).
  require_docker
  require_network
  require_image "${DOCS_IMAGE}" "context-kit build"

  if ! docker ps --filter "name=^${DOCS_CONTAINER_NAME}$" --filter "status=running" --format '{{.Names}}' | grep -qx "${DOCS_CONTAINER_NAME}"; then
    fail "long-lived docs-mcp not running; start it with: context-kit start"
  fi

  local bridge_url="http://${DOCS_CONTAINER_NAME}:8000/mcp"
  exec docker run --rm -i \
    --label dev.context-kit=true \
    --network "${NETWORK}" \
    --entrypoint mcp-proxy \
    "${DOCS_IMAGE}" \
    --transport streamablehttp \
    "${bridge_url}"
}

cmd_repomix() {
  require_docker
  require_image "${REPOMIX_IMAGE}" "docker pull ${REPOMIX_IMAGE}"
  local dir mount_dir
  dir="$(project_dir)"
  mount_dir="${CONTEXT_KIT_REPOMIX_MOUNT_DIR:-${dir}}"
  mount_dir="$(cd "${mount_dir}" && pwd -P)"
  exec docker run --rm -i \
    --label dev.context-kit=true \
    -v "${mount_dir}:${mount_dir}:ro" \
    --workdir "${dir}" \
    "${REPOMIX_IMAGE}" --mcp
}

snippet_command() {
  case "${1:-}" in
    --absolute) printf '%s' "${ROOT}/bin/context-kit" ;;
    "") printf '%s' "context-kit" ;;
    *) fail "unknown install option: ${1}" ;;
  esac
}

print_opencode() {
  local bin url
  bin="$(json_escape "$(snippet_command "${1:-}")")"
  url="$(json_escape "${DOCS_HTTP_URL}")"
  cat <<JSON
{
  "\$schema": "https://opencode.ai/config.json",
  "mcp": {
    "context-web-search": {
      "type": "local",
      "command": ["${bin}", "web-search"],
      "enabled": true,
      "timeout": 60000
    },
    "context-docs": {
      "type": "remote",
      "url": "${url}",
      "enabled": true
    },
    "context-repomix": {
      "type": "local",
      "command": ["${bin}", "repomix"],
      "enabled": true,
      "timeout": 120000
    }
  }
}
JSON
}

print_claude() {
  local bin url
  bin="$(json_escape "$(snippet_command "${1:-}")")"
  url="$(json_escape "${DOCS_HTTP_URL}")"
  cat <<JSON
{
  "mcpServers": {
    "context-web-search": {
      "command": "${bin}",
      "args": ["web-search"]
    },
    "context-docs": {
      "type": "http",
      "url": "${url}"
    },
    "context-repomix": {
      "command": "${bin}",
      "args": ["repomix"]
    }
  }
}
JSON
}

cmd_install() {
  local target="${1:-}"
  shift || true
  case "${target}" in
    opencode) print_opencode "${1:-}" ;;
    claude) print_claude "${1:-}" ;;
    *) fail "usage: context-kit install claude|opencode [--absolute]" ;;
  esac
}

cmd_redaction_check() {
  local bad=0
  local local_path_terms='/(home|Users)/[^/[:space:]]+|[A-Za-z]:\\Users\\[^\\[:space:]]+'
  local secret_terms='AKIA[0-9A-Z]{16}|BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY|xox[baprs]-|sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|gitea_[A-Za-z0-9_-]{20,}'

  # Scan only what would be published: skip .git plus everything .gitignore
  # excludes by convention (local .env files, caches, logs).
  local grep_opts=(
    -RInE
    --exclude-dir=.git
    --exclude-dir=.cache
    --exclude-dir=tmp
    --exclude=.env
    --exclude=.env.local
    --exclude=*.log
  )

  if grep "${grep_opts[@]}" "${local_path_terms}" "${ROOT}"; then
    bad=1
  fi
  if grep "${grep_opts[@]}" "${secret_terms}" "${ROOT}"; then
    bad=1
  fi

  if [[ "${bad}" -eq 0 ]]; then
    printf 'pass redaction-check found no local absolute paths or common secret patterns\n'
  else
    printf 'fail redaction-check found blocked content\n' >&2
  fi
  return "${bad}"
}

case "${1:-}" in
  start) shift; cmd_start "$@" ;;
  stop) shift; cmd_stop "$@" ;;
  restart) shift; cmd_stop; cmd_start "$@" ;;
  build) shift; cmd_build "$@" ;;
  status) shift; cmd_status "$@" ;;
  doctor) shift; cmd_doctor "$@" ;;
  web-search) shift; cmd_web_search "$@" ;;
  docs) shift; cmd_docs "$@" ;;
  repomix) shift; cmd_repomix "$@" ;;
  install) shift; cmd_install "$@" ;;
  redaction-check) shift; cmd_redaction_check "$@" ;;
  -h|--help|help|"") usage ;;
  *) usage >&2; exit 64 ;;
esac
