#!/usr/bin/env bash
# kv — recipient-facing wrapper for kv-setup.
#
# Subcommands:
#   init                       run questionnaire + bootstrap
#   up                         start the stack (docker compose up -d --build)
#   down                       stop the stack
#   status                     health checks (containerized + launchd MLX)
#   doctor                     diagnose prereqs + stack health (read-only)
#   repair                     apply safe auto-fixes (start/restart/reload)
#   reminders                  show the in-vault digest (reminders + health)
#   logs [service]             docker compose logs OR launchd tail
#   reload [domain]            POST /reload on each (or named) indexer
#   domains                    list configured domains + indexer ports
#   ingest add <url> [domain]  append URL to the domain's queue
#   ingest file <path> [dom]   ingest a local PDF/EPUB into a domain
#   ingest run [domain]        one-shot ingest pass
#   note <domain> "<title>"    create a manual note in a domain (Topics/)
#   inbox [list|clean]         list / remove ingested PDF/EPUB originals (_inbox)
#   uninstall [--yes]          remove containers+volumes+launchd; keeps your vault
#   help                       this help

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$REPO_ROOT"

ENV_FILE="${REPO_ROOT}/.env"

# Source .env if present so subcommands inherit INGEST_DOMAINS etc.
if [[ -f "$ENV_FILE" ]]; then
    set -a; source "$ENV_FILE"; set +a
fi

INGEST_DOMAINS="${INGEST_DOMAINS:-}"
KV_VAULT_PATH="${KV_VAULT_PATH:-./kv-vault}"
EMBEDDING_BACKEND="${EMBEDDING_BACKEND:-torch}"
MCP_PORT="${MCP_PORT:-9001}"

# --- helpers ----------------------------------------------------------------

die() { echo "kv: $*" >&2; exit 1; }

need() { command -v "$1" > /dev/null 2>&1 || die "missing prereq: $1"; }

mlx_mode() { [[ "$EMBEDDING_BACKEND" == "mlx" ]]; }

# Portable single-instance guard for mutating operations. macOS ships no
# flock(1), so we use an atomic mkdir lock with a PID file and steal it only if
# the holder is dead. This stops two `kv up`/`down`/`init` runs from racing.
LOCK_DIR="${TMPDIR:-/tmp}/kv-setup.lock.d"

# Only remove the lock if WE still own it — prevents a delayed trap from
# deleting a lock another process legitimately acquired after stealing ours.
release_lock() {
    [[ -f "$LOCK_DIR/pid" && "$(cat "$LOCK_DIR/pid" 2>/dev/null || true)" == "$$" ]] && rm -rf "$LOCK_DIR"
}

acquire_lock() {
    if ! mkdir "$LOCK_DIR" 2>/dev/null; then
        local pid; pid="$(cat "$LOCK_DIR/pid" 2>/dev/null || true)"
        if [[ -n "$pid" ]] && ! kill -0 "$pid" 2>/dev/null; then
            rm -rf "$LOCK_DIR"
            mkdir "$LOCK_DIR" 2>/dev/null || die "could not acquire lock at $LOCK_DIR"
        else
            die "another kv command is still running (pid ${pid:-unknown}) — wait for it to finish and try again. (If you're sure nothing is running, your assistant can clear the stale lock at $LOCK_DIR.)"
        fi
    fi
    echo "$$" > "$LOCK_DIR/pid"
    trap 'release_lock' EXIT
    # Re-verify ownership after a possible steal race; bail rather than proceed
    # if another process clobbered us between mkdir and write.
    [[ "$(cat "$LOCK_DIR/pid" 2>/dev/null || true)" == "$$" ]] || die "lost the lock to a concurrent kv operation; try again."
}

# True if the compose stack has at least one running container.
stack_running() { [[ -n "$(docker compose ps -q 2>/dev/null)" ]]; }

# MCP liveness: any HTTP response (even 4xx) on /mcp means the server is up.
# A bare GET on a streamable-HTTP endpoint is not a clean 200, so we don't -f.
mcp_alive() { curl -s -o /dev/null -m 3 "http://localhost:${MCP_PORT}/mcp"; }

domain_port() {
    # Read the docker-compose.override.yaml to find the indexer port for a domain.
    # Fallback: 8001 for first domain enumerated in INGEST_DOMAINS, 8002 next, ...
    local domain="$1"
    local i=1
    local IFS=,
    for d in $INGEST_DOMAINS; do
        if [[ "$d" == "$domain" ]]; then
            echo "$((8000 + i))"
            return 0
        fi
        i=$((i + 1))
    done
    return 1
}

each_domain() {
    local IFS=,
    for d in $INGEST_DOMAINS; do
        echo "$d"
    done
}

# Resolve which domain to use. If given, echo it. If exactly one is configured,
# default to it. If several, fail (so we never silently misfile into the wrong
# domain). Caller pattern: `domain="$(resolve_domain "$arg")" || exit 1`.
resolve_domain() {
    local d="$1"
    if [[ -n "$d" ]]; then echo "$d"; return 0; fi
    local doms=() x
    while IFS= read -r x; do [[ -n "$x" ]] && doms+=("$x"); done < <(each_domain)
    if [[ "${#doms[@]}" -eq 1 ]]; then echo "${doms[0]}"; return 0; fi
    if [[ "${#doms[@]}" -eq 0 ]]; then
        echo "kv: no domains configured — run 'kv init'" >&2; return 1
    fi
    echo "kv: multiple domains configured (${doms[*]}); name one, e.g. 'kv ingest add <url> <domain>' — see 'kv domains'." >&2
    return 1
}

# --- subcommands ------------------------------------------------------------

cmd_help() {
    # Print the leading comment block up to (but not including) the blank line
    # that separates the help banner from `set -euo pipefail`.
    awk '/^set / { exit } NR > 1 { sub(/^# ?/, ""); print }' "$0"
}

cmd_init() {
    need uv
    acquire_lock
    uv run --project setup kv-setup-init "$@"
}

cmd_up() {
    need docker
    acquire_lock
    echo "Starting kv. The FIRST run downloads/builds ~2GB (images + the search"
    echo "model) and can take 10-15 minutes — this is normal, please wait. Later"
    echo "starts take seconds."
    # --remove-orphans drops services that vanished from the override (e.g. the
    # docker indexer when switching to MLX), so we never run duplicate indexers.
    docker compose up -d --build --remove-orphans "$@"
}

cmd_down() {
    need docker
    acquire_lock
    docker compose down "$@"
}

cmd_status() {
    echo "== docker compose =="
    docker compose ps 2>&1 || echo "(docker compose not running)"
    if mlx_mode; then
        echo
        echo "== launchd (MLX hybrid) =="
        for d in $(each_domain); do
            local label="com.shourjoguha.kv-indexer-${d}"
            local state
            state="$(launchctl print "gui/$(id -u)/${label}" 2>/dev/null | awk '/state =/ {print $3; exit}')"
            printf "  %-40s %s\n" "$label" "${state:-not loaded}"
        done
    fi
    echo
    echo "== HTTP health =="
    for d in $(each_domain); do
        local port
        port="$(domain_port "$d")" || continue
        if curl -sf -m 2 "http://localhost:${port}/health" > /dev/null 2>&1; then
            printf "  indexer-%-20s http://localhost:%s OK\n" "$d" "$port"
        else
            printf "  indexer-%-20s http://localhost:%s DOWN\n" "$d" "$port"
        fi
    done
    if mcp_alive; then
        echo "  mcp                          http://localhost:${MCP_PORT}/mcp OK"
    else
        echo "  mcp                          http://localhost:${MCP_PORT}/mcp DOWN (try 'kv repair' or 'kv logs mcp')"
    fi

    local digest="${KV_VAULT_PATH}/_kv-digest.md"
    if [[ -f "$digest" ]]; then
        echo
        echo "== reminders == (full: 'kv reminders')"
        grep -E '^- [0-9]' "$digest" | head -3 || true
    fi
}

cmd_reminders() {
    local digest="${KV_VAULT_PATH}/_kv-digest.md"
    [[ -f "$digest" ]] || die "no digest yet at $digest (it appears once the scheduler runs)"
    cat "$digest"
}

cmd_logs() {
    local svc="${1:-}"
    if mlx_mode && [[ "$svc" == "indexer" || "$svc" == indexer-* ]]; then
        local log_dir="${HOME}/Library/Logs/kv-setup"
        local domain="${svc#indexer-}"
        if [[ "$svc" == "indexer" ]]; then
            tail -F "${log_dir}/indexer-"*.log
        else
            tail -F "${log_dir}/indexer-${domain}.log"
        fi
        return 0
    fi
    if [[ -z "$svc" ]]; then
        docker compose logs -f --tail=100
    else
        docker compose logs -f --tail=100 "$svc"
    fi
}

cmd_reload() {
    acquire_lock
    local target_domain="${1:-}"
    for d in $(each_domain); do
        if [[ -n "$target_domain" && "$d" != "$target_domain" ]]; then
            continue
        fi
        local port
        port="$(domain_port "$d")" || continue
        printf "  reloading indexer-%s (:%s) ... " "$d" "$port"
        if curl -sf -m 30 -X POST "http://localhost:${port}/reload" > /dev/null; then
            echo "OK"
        else
            echo "FAIL"
        fi
    done
}

cmd_ingest() {
    local sub="${1:-}"
    shift || true
    case "$sub" in
        add)
            local url="${1:-}"
            local domain="${2:-}"
            [[ -z "$url" ]] && die "usage: kv ingest add <url> [domain]"
            domain="$(resolve_domain "$domain")" || exit 1
            local queue="${KV_VAULT_PATH}/Videos/${domain}/_ingest_queue.md"
            [[ -f "$queue" ]] || die "queue not found: $queue (is '$domain' a configured domain? see 'kv domains')"
            # Locked, deduped append. When the stack is up, route through the
            # container so the lock is taken in the same kernel as the worker
            # (host<->container flock isn't reliable on macOS). When it's down,
            # append host-side — the worker isn't running, so nothing races.
            # Fall back to the host path if the container append is unavailable
            # (e.g. an older image built before kv_ingest.queue_add existed).
            if stack_running && docker compose exec -T ingest python -m kv_ingest.queue_add \
                    "/vault/Videos/${domain}/_ingest_queue.md" "$url" --domain "$domain"; then
                :
            else
                need uv
                uv run --project setup python -m kv_setup.queue_add "$queue" "$url" --domain "$domain"
            fi
            ;;
        file)
            local path="${1:-}"
            local domain="${2:-}"
            [[ -z "$path" ]] && die "usage: kv ingest file <path-to-.pdf-or-.epub> [domain]"
            [[ -f "$path" ]] || die "file not found: $path"
            domain="$(resolve_domain "$domain")" || exit 1
            need docker
            stack_running || die "stack not running — run 'kv up' first (file ingestion runs inside the container)"
            local ext="${path##*.}"; ext="${ext,,}"
            local module
            case "$ext" in
                pdf)  module="kv_ingest.pdf" ;;
                epub) module="kv_ingest.epub" ;;
                *)    die "unsupported file type: .$ext (supported: pdf, epub)" ;;
            esac
            # Copy into the vault's _inbox so the container can read it (the vault
            # is the only host dir bind-mounted in). _inbox is NOT indexed.
            # Warn (don't block) if Books isn't in this domain's index scope.
            # Advisory only — never let a missing uv stop the actual ingestion.
            if command -v uv > /dev/null 2>&1; then
                uv run --project setup python -m kv_setup.domain_util "$KV_VAULT_PATH" "$domain" Books || true
            fi
            local base inbox
            base="$(basename "$path")"
            inbox="${KV_VAULT_PATH}/_inbox"
            mkdir -p "$inbox"
            cp "$path" "${inbox}/${base}"
            echo "ingesting ${base} into domain '${domain}'..."
            docker compose exec -T ingest python -m "$module" --path "/vault/_inbox/${base}" --domain "$domain"
            local port
            if port="$(domain_port "$domain")"; then
                curl -sf -m 60 -X POST "http://localhost:${port}/reload" > /dev/null 2>&1 \
                    && echo "reloaded indexer-${domain}" || echo "(reload skipped — run 'kv reload ${domain}' once the indexer is ready)"
            fi
            ;;
        run)
            acquire_lock
            local domain="${1:-}"
            need docker
            if [[ -z "$domain" ]]; then
                # One-shot pass across all domains (the worker keeps running).
                docker compose exec -T ingest kv-ingest-cron --once
                return $?
            fi
            local port
            port="$(domain_port "$domain")" || die "unknown domain: $domain"
            docker compose exec -T ingest python -m kv_ingest.queue \
                --queue "Videos/${domain}/_ingest_queue.md" \
                --rel-dir-prefix "Videos/${domain}" \
                --rel-dir-article "Newsletters/${domain}" \
                --reload-url "http://indexer-${domain}:${port}/reload"
            ;;
        *)
            die "unknown ingest subcommand: $sub (try: add, file, run)"
            ;;
    esac
}

cmd_domains() {
    [[ -n "$INGEST_DOMAINS" ]] || die "no domains configured — run 'kv init'"
    echo "Configured domains:"
    for d in $(each_domain); do
        local port
        port="$(domain_port "$d")" || port="?"
        printf "  %-20s indexer :%s\n" "$d" "$port"
    done
}

cmd_note() {
    local domain="${1:-}"
    local title="${2:-}"
    local body_file="${3:-}"
    [[ -n "$title" ]] || die "usage: kv note <domain> \"<title>\" [body-file]"
    domain="$(resolve_domain "$domain")" || exit 1
    need uv
    local args=("$domain" "$title" --vault "$KV_VAULT_PATH")
    [[ -n "$body_file" ]] && args+=(--body-file "$body_file")
    local note_path
    note_path="$(uv run --project setup python -m kv_setup.note_add "${args[@]}")" || die "note creation failed"
    echo "wrote $note_path"
    # Make it searchable.
    local port
    if port="$(domain_port "$domain")"; then
        curl -sf -m 60 -X POST "http://localhost:${port}/reload" > /dev/null 2>&1 \
            && echo "reloaded indexer-${domain}" || echo "(reload skipped — run 'kv reload ${domain}' when the stack is up)"
    fi
}

cmd_inbox() {
    local sub="${1:-list}"
    local inbox="${KV_VAULT_PATH}/_inbox"
    case "$sub" in
        list)
            if [[ -d "$inbox" && -n "$(ls -A "$inbox" 2>/dev/null)" ]]; then
                du -sh "$inbox" 2> /dev/null || true
                ls -1 "$inbox"
            else
                echo "no original files in _inbox"
            fi
            ;;
        clean)
            [[ -d "$inbox" ]] || { echo "nothing to clean"; return 0; }
            echo "removing originals in $inbox — already-ingested notes keep a path"
            echo "breadcrumb to these files (for re-opening PDFs); that link will break."
            rm -rf "${inbox:?}/"* 2> /dev/null || true
            echo "cleaned $inbox"
            ;;
        *)
            die "usage: kv inbox [list|clean]"
            ;;
    esac
}

cmd_uninstall() {
    if [[ "${1:-}" != "--yes" ]]; then
        echo "kv uninstall will:"
        echo "  - stop and REMOVE all kv containers and volumes (search indexes +"
        echo "    downloaded models are deleted, and rebuilt on the next 'kv up')"
        echo "  - remove the launchd indexer jobs (MLX mode)"
        echo "  - delete the generated .env and docker-compose.override.yaml"
        echo
        echo "Your vault (your notes) at '${KV_VAULT_PATH}' is NOT touched."
        echo "Re-run with:  ./kv uninstall --yes"
        return 0
    fi
    acquire_lock
    need docker
    echo "removing containers + volumes..."
    docker compose down -v --remove-orphans || true
    local agents="${HOME}/Library/LaunchAgents"
    if [[ -d "$agents" ]]; then
        local uid plist label
        uid="$(id -u)"
        for plist in "$agents"/com.shourjoguha.kv-indexer-*.plist; do
            [[ -e "$plist" ]] || continue
            label="$(basename "$plist" .plist)"
            launchctl bootout "gui/${uid}/${label}" 2> /dev/null || true
            rm -f "$plist"
            echo "  removed launchd job ${label}"
        done
    fi
    rm -f "${REPO_ROOT}/.env" "${REPO_ROOT}/docker-compose.override.yaml"
    echo "done. Your vault is intact at: ${KV_VAULT_PATH}"
    echo "To start fresh:  ./kv init && ./kv up"
}

cmd_doctor() {
    # Read-only diagnosis. Pairs with `kv repair` (which applies the safe fixes).
    local ok=1
    echo "== prerequisites =="
    for tool in docker uv curl; do
        if command -v "$tool" > /dev/null 2>&1; then
            printf "  [ok] %s -> %s\n" "$tool" "$(command -v "$tool")"
        else
            printf "  [MISSING] %s — install it before continuing\n" "$tool"
            ok=0
        fi
    done
    if command -v npx > /dev/null 2>&1; then
        echo "  [ok] npx (needed for the Claude Desktop MCP bridge)"
    else
        echo "  [warn] npx not found — Claude Desktop can't reach kv-mcp until Node.js is installed (Claude Code is unaffected)"
    fi

    echo
    echo "== config =="
    [[ -f "$ENV_FILE" ]] && echo "  [ok] .env present" || { echo "  [MISSING] .env — run: kv init"; ok=0; }
    [[ -f "${REPO_ROOT}/docker-compose.override.yaml" ]] && echo "  [ok] docker-compose.override.yaml present" \
        || { echo "  [MISSING] docker-compose.override.yaml — run: kv init"; ok=0; }

    echo
    echo "== runtime =="
    if command -v docker > /dev/null 2>&1; then
        if stack_running; then
            echo "  [ok] stack is up"
        else
            echo "  [down] stack is not running — run: kv up   (or: kv repair)"
            ok=0
        fi
        mcp_alive && echo "  [ok] mcp reachable on :${MCP_PORT}" \
            || { echo "  [down] mcp not reachable on :${MCP_PORT} — run: kv repair"; ok=0; }
    fi

    if [[ $ok -eq 1 ]]; then
        echo
        echo "All clear. 'kv repair' is available if something goes down later."
    fi
    return $((1 - ok))
}

cmd_repair() {
    # Hybrid policy: only SAFE, non-destructive fixes here. Anything that could
    # touch data or re-run setup (kv init, rebuilds-from-scratch) is left to the
    # human / an explicit command.
    need docker
    acquire_lock
    [[ -f "$ENV_FILE" ]] || die ".env missing — run 'kv init' first (repair won't run setup for you)"

    if ! stack_running; then
        echo "stack down → starting it..."
        docker compose up -d --build --remove-orphans
    else
        echo "stack already running."
    fi

    if ! mcp_alive; then
        echo "mcp not responding → restarting mcp service..."
        docker compose restart mcp || true
    fi

    echo "reloading healthy indexers..."
    # cmd_reload acquires the lock itself; inline here to avoid re-locking.
    # Only reload an indexer that already answers /health — reloading one that's
    # still booting or downloading models would interrupt that work.
    for d in $(each_domain); do
        local port
        port="$(domain_port "$d")" || continue
        if ! curl -sf -m 5 "http://localhost:${port}/health" > /dev/null 2>&1; then
            printf "  indexer-%s (:%s) not ready — skipping reload (it may be downloading models; check 'kv logs indexer-%s')\n" "$d" "$port" "$d"
            continue
        fi
        printf "  reload indexer-%s (:%s) ... " "$d" "$port"
        if curl -sf -m 60 -X POST "http://localhost:${port}/reload" > /dev/null; then
            echo "OK"
        else
            echo "FAIL (see 'kv logs indexer-${d}')"
        fi
    done

    echo
    echo "repair pass done — run 'kv status' to confirm. If services are still"
    echo "down, check 'kv logs', or re-run setup with 'kv init'."
}

# --- dispatch ---------------------------------------------------------------

main() {
    local sub="${1:-help}"
    shift || true
    case "$sub" in
        help|-h|--help) cmd_help ;;
        init)           cmd_init "$@" ;;
        up)             cmd_up "$@" ;;
        down)           cmd_down "$@" ;;
        status)         cmd_status "$@" ;;
        logs)           cmd_logs "$@" ;;
        reload)         cmd_reload "$@" ;;
        ingest)         cmd_ingest "$@" ;;
        domains)        cmd_domains "$@" ;;
        note)           cmd_note "$@" ;;
        inbox)          cmd_inbox "$@" ;;
        uninstall)      cmd_uninstall "$@" ;;
        doctor)         cmd_doctor "$@" ;;
        repair)         cmd_repair "$@" ;;
        reminders)      cmd_reminders "$@" ;;
        *)              die "unknown subcommand: $sub (try: kv help)" ;;
    esac
}

main "$@"
