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

# ─── Bash version check ─────────────────────────────────────────────────────
# macOS ships Bash 3.2 (2007). Kronn needs Bash 4+ for some features.
if (( BASH_VERSINFO[0] < 4 )); then
    echo ""
    printf "\033[0;33m  ⚠  Bash %s detected — version 4+ recommended.\033[0m\n" "$BASH_VERSION"
    echo ""
    if command -v brew >/dev/null 2>&1; then
        printf "\033[0;36m  Install via Homebrew:\033[0m\n"
        printf "\033[2m    brew install bash\033[0m\n"
        printf "\033[2m    Then relaunch: /opt/homebrew/bin/bash %s\033[0m\n" "$(printf '%q ' "$0" "$@")"
    else
        printf "\033[0;36m  Install Homebrew first:\033[0m\n"
        printf '\033[2m    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"\033[0m\n'
        printf "\033[2m    brew install bash\033[0m\n"
    fi
    echo ""
    printf "\033[0;33m  Attempting to start in compatibility mode...\033[0m\n"
    echo ""
fi

# ─── Resolve kronn's own directory (follow symlinks) ─────────────────────────
KRONN_SOURCE="${BASH_SOURCE[0]}"
while [[ -L "$KRONN_SOURCE" ]]; do
    KRONN_SOURCE="$(readlink "$KRONN_SOURCE")"
done
KRONN_DIR="$(cd "$(dirname "$KRONN_SOURCE")" && pwd)"
KRONN_CONFIG_DIR="${HOME}/.config/kronn"

# ─── Source libraries ────────────────────────────────────────────────────────
source "$KRONN_DIR/lib/ui.sh"
source "$KRONN_DIR/lib/agents.sh"
source "$KRONN_DIR/lib/mcps.sh"
source "$KRONN_DIR/lib/tron.sh"
source "$KRONN_DIR/lib/analyze.sh"
source "$KRONN_DIR/lib/repos.sh"
source "$KRONN_DIR/lib/api-client.sh"

# ─── Config ──────────────────────────────────────────────────────────────────

load_config() {
    local config_file="$KRONN_CONFIG_DIR/config.toml"
    mkdir -p "$KRONN_CONFIG_DIR"

    SCAN_PATH=""

    if [[ -f "$config_file" ]]; then
        SCAN_PATH=$(awk -F'"' '/^scan_path/ {print $2}' "$config_file" 2>/dev/null || true)
    fi

    # Default: parent directory of kronn repo
    if [[ -z "$SCAN_PATH" ]]; then
        SCAN_PATH="$(dirname "$KRONN_DIR")"
    fi
}

save_config() {
    mkdir -p "$KRONN_CONFIG_DIR"
    cat > "$KRONN_CONFIG_DIR/config.toml" <<TOML
# Kronn configuration
scan_path = "$SCAN_PATH"
agent = "${SELECTED_AGENT:-}"
TOML
}

kiro_container_available() {
    (cd "$KRONN_DIR" && docker compose exec -T backend /bin/sh -lc \
        'PATH="$HOME/.local/bin:$PATH"; command -v kiro-cli >/dev/null 2>&1') >/dev/null 2>&1
}

kiro_container_authenticated() {
    (cd "$KRONN_DIR" && docker compose exec -T backend /bin/sh -lc \
        'PATH="$HOME/.local/bin:$PATH"; kiro-cli whoami --format json >/dev/null 2>&1') >/dev/null 2>&1
}

maybe_login_kiro() {
    # Kiro container bootstrap is required on macOS hosts.
    [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]] || return 0

    if ! kiro_container_available; then
        warn "kiro-cli is not available in the container (installation in progress or failed)."
        return 0
    fi

    info "Kiro detected — checking authentication..."
    if kiro_container_authenticated; then
        success "Kiro already authenticated."
        return 0
    fi

    warn "Kiro not authenticated in the container."
    info "Starting Kiro login (device flow)..."
    if ! (cd "$KRONN_DIR" && make kiro-login); then
        warn "Kiro login not completed. You can retry: make kiro-login"
    fi
}

# ─── Commands ────────────────────────────────────────────────────────────────

cmd_start() {
    banner

    # Present the choice FIRST — agent detection is slow (~5-10s) and
    # completely unnecessary when the user just wants to launch the web UI.
    # The web interface has its own detection (instant, via the backend API).
    menu_choice "How would you like to use Kronn?" \
        "Launch web interface (recommended)" \
        "Continue in CLI"

    if [[ "$REPLY" == "1" ]]; then
        cmd_web
        return
    fi

    # CLI flow: detect agents + scan repos (only needed for the CLI path)
    select_agent
    cmd_repos
}

cmd_web() {
    step "Starting web interface"

    # Check docker
    if ! command -v docker >/dev/null 2>&1; then
        fail "Docker is not installed."
        printf "  ${DIM}https://docs.docker.com/get-docker/${RESET}\n"
        return 1
    fi

    # Check make
    if ! command -v make >/dev/null 2>&1; then
        warn "make is not installed."
        if ask_yn "Install make?"; then
            install_make
        else
            fail "make is required for the web interface."
            return 1
        fi
    fi

    info "Build & starting..."
    # KRONN_DEBUG=1 in the environment (set by `./kronn start --debug` upstream)
    # propagates to `make start` as DEBUG=1, which writes KRONN_RUST_LOG=…=debug
    # into .env for this run only. Config.server.debug_mode remains untouched.
    local make_args=()
    local debug_requested=0
    if [[ "${KRONN_DEBUG:-0}" == "1" ]]; then
        make_args+=("DEBUG=1")
        debug_requested=1
        info "Debug mode requested (--debug) — backend will log at debug level."
    fi
    (cd "$KRONN_DIR" && make start ${make_args[@]+"${make_args[@]}"})
    # Backend is now running — reset the API availability cache so
    # subsequent commands in this session (status, agents, etc.) use
    # the API instead of slow local fallback.
    kronn_api_reset_cache
    maybe_login_kiro

    # Containers run detached (`docker compose up -d`) so there's no log
    # stream on the terminal by default. Always point the user at the logs
    # command; when they passed --debug they clearly want to SEE them, so
    # auto-tail in that case (Ctrl+C detaches without stopping the app).
    echo ""
    if (( debug_requested == 1 )); then
        info "Debug active — streaming logs now (Ctrl+C to detach, containers keep running)."
        printf "  ${DIM}Useful filters once attached:${RESET}\n"
        printf "  ${DIM}  kronn logs | grep 'kronn::agent_detect'   — agent detection trace${RESET}\n"
        printf "  ${DIM}  kronn logs | grep 'kronn::scanner'        — project scan + host-path mapping${RESET}\n"
        echo ""
        (cd "$KRONN_DIR" && make logs) || true
    else
        info "Logs: run ${BOLD}kronn logs${RESET} (or ${BOLD}make logs${RESET}) to tail them."
        printf "  ${DIM}Path: stdout of the Docker containers (no file on disk).${RESET}\n"
    fi
}

cmd_repos() {
    step "Repository scan"

    info "Scanning: $SCAN_PATH"
    scan_repos "$SCAN_PATH"

    if [[ ${#REPO_PATHS[@]} -eq 0 ]]; then
        warn "No git repository found in $SCAN_PATH"
        return
    fi

    success "${#REPO_PATHS[@]} repository(ies) found"

    # Loop: let user pick a repo, then choose what to do with it.
    while true; do
        select_repo
        local chosen=$REPLY

        if [[ "$chosen" == "0" ]]; then
            save_config
            cmd_web
            return
        fi

        local idx=$((chosen - 1))
        local repo_dir="${REPO_PATHS[$idx]}"
        local repo_name="${REPO_NAMES[$idx]}"

        _project_action_menu "$repo_dir" "$repo_name" "$idx"
    done
}

# Sub-menu for a selected project — mirrors the web dashboard's project card.
# Actions adapt to the current state: template install only shows when needed,
# audit/briefing only when the backend is running, etc.
_project_action_menu() {
    local repo_dir="$1" repo_name="$2" list_idx="$3"

    while true; do
        local repo_status
        repo_status=$(detect_ai_context "$repo_dir")
        REPO_STATUS[$list_idx]="$repo_status"

        echo
        step "$repo_name"
        local color="$DIM"
        case "$repo_status" in
            Validated*) color="$GREEN" ;;
            Audited*)   color="$GREEN" ;;
            Template*)  color="$YELLOW" ;;
            *MCP*)      color="$CYAN" ;;
        esac
        printf "  Status: ${color}%s${RESET}\n" "$repo_status"
        echo

        local -a options=()
        local -a keys=()

        # Template install — show when no ai/ or not yet audited
        case "$repo_status" in
            "not configured"|Template*)
                options+=("${GREEN}▸${RESET} Install AI template")
                keys+=("template")
                ;;
        esac

        # Backend-dependent actions (audit, briefing, open dashboard)
        if kronn_api_available; then
            case "$repo_status" in
                Template*|Audited*)
                    options+=("${CYAN}▸${RESET} Launch AI audit ${DIM}(opens web dashboard)${RESET}")
                    keys+=("audit")
                    ;;
            esac
            case "$repo_status" in
                "not configured"|Template*)
                    options+=("${CYAN}▸${RESET} Launch briefing ${DIM}(opens web dashboard)${RESET}")
                    keys+=("briefing")
                    ;;
            esac
            options+=("${CYAN}▸${RESET} View plugins / MCPs ${DIM}(opens web dashboard)${RESET}")
            keys+=("mcps")
            options+=("${CYAN}▸${RESET} Open project in dashboard")
            keys+=("dashboard")
        else
            # Local MCP view fallback
            local mcp_file="$repo_dir/.mcp.json"
            if [[ -f "$mcp_file" ]]; then
                local mcount
                mcount=$(grep -c '"command"' "$mcp_file" 2>/dev/null || echo 0)
                options+=("${DIM}▸${RESET} View MCPs (${mcount} configured, .mcp.json)")
                keys+=("mcps_local")
            fi
        fi

        options+=("${DIM}← Back to project list${RESET}")
        keys+=("back")

        menu_choice "Action:" "${options[@]}"
        local action="${keys[$((REPLY-1))]}"

        # Clear the interactive menu remnants before running the action —
        # menu_choice uses ANSI cursor tricks that leave ghost lines on
        # screen. A simple terminal reset (scroll region + clear below)
        # prevents the "text printed on top of the menu" visual glitch.
        printf "\033[J"

        case "$action" in
            template)
                init_repo "$repo_dir"
                echo
                printf "  ${DIM}Press Enter to continue...${RESET}"
                read -r
                ;;
            audit|briefing|mcps|dashboard)
                _open_project_in_browser "$repo_name" "$action"
                echo
                printf "  ${DIM}Press Enter to continue...${RESET}"
                read -r
                ;;
            mcps_local)
                _show_local_mcps "$repo_dir"
                printf "  ${DIM}Press Enter to continue...${RESET}"
                read -r
                ;;
            back)
                break
                ;;
        esac
    done
}

# Open the web dashboard on the right project. Uses the API to resolve
# the project ID from its name, then opens `/#project-<id>` so the
# Dashboard auto-expands and scrolls to the matching card.
_open_project_in_browser() {
    local repo_name="$1" action="$2"
    local base_url="http://localhost:3140"
    local url="$base_url"

    # Try to resolve the project ID for deep-linking.
    if kronn_api_available; then
        local project_id=""
        if command -v jq >/dev/null 2>&1; then
            project_id=$(kronn_api_data GET /projects 2>/dev/null \
                | jq -r --arg name "$repo_name" '.[] | select(.name == $name) | .id' 2>/dev/null \
                | head -1)
        elif command -v python3 >/dev/null 2>&1; then
            project_id=$(kronn_api_data GET /projects 2>/dev/null \
                | python3 -c "
import sys, json
name = '$repo_name'
for p in json.load(sys.stdin):
    if p['name'] == name:
        print(p['id']); break
" 2>/dev/null)
        fi
        if [[ -n "$project_id" ]]; then
            url="${base_url}/#project-${project_id}"
        fi
    fi

    info "Opening $repo_name in dashboard..."
    printf "  ${DIM}%s${RESET}\n" "$url"

    # Cross-platform open: wslview (WSL) / xdg-open (Linux) / open (macOS)
    if command -v wslview >/dev/null 2>&1; then
        wslview "$url" 2>/dev/null &
    elif command -v xdg-open >/dev/null 2>&1; then
        xdg-open "$url" 2>/dev/null &
    elif command -v open >/dev/null 2>&1; then
        open "$url" 2>/dev/null &
    else
        info "Open this URL in your browser: $url"
    fi
}

# Quick view of .mcp.json when the backend isn't running.
_show_local_mcps() {
    local repo_dir="$1"
    local mcp_file="$repo_dir/.mcp.json"

    if [[ ! -f "$mcp_file" ]]; then
        warn "No .mcp.json in this project."
        return
    fi

    echo
    info "MCPs from .mcp.json:"
    # Extract server names from the JSON — lightweight, no jq dependency.
    # Pattern: keys inside "mcpServers": { "name": { ... }, ... }
    if command -v jq >/dev/null 2>&1; then
        jq -r '.mcpServers // {} | keys[]' "$mcp_file" 2>/dev/null | while read -r name; do
            printf "  ${GREEN}●${RESET} %s\n" "$name"
        done
    elif command -v python3 >/dev/null 2>&1; then
        python3 -c "
import json, sys
try:
    d = json.load(open('$mcp_file'))
    for k in d.get('mcpServers', {}):
        print(f'  ● {k}')
except: pass
"
    else
        printf "  ${DIM}(install jq or python3 to view MCP names)${RESET}\n"
        printf "  ${DIM}%s${RESET}\n" "$mcp_file"
    fi
    echo
}

cmd_mcp() {
    local subcmd="${1:-sync}"
    case "$subcmd" in
        sync)
            load_config
            scan_repos "$SCAN_PATH"
            sync_mcp_all
            ;;
        check)
            step "MCP prerequisites"
            check_mcp_prereqs
            ;;
        secrets)
            init_secrets
            ;;
        *)
            echo "Usage: kronn mcp [sync|check|secrets]"
            ;;
    esac
}

cmd_status() {
    banner

    # When the backend is running, delegate to the API — instant, complete
    # (includes Ollama, debug_mode, DB stats, etc.), zero duplication.
    if kronn_api_available; then
        step "Kronn backend"
        kronn_api_show_health

        step "Agents (from backend)"
        kronn_api_show_agents || warn "Failed to query agents API"

        step "Database"
        kronn_api_show_status || warn "Failed to query DB info API"

        return
    fi

    # Fallback: backend not running — use local shell detection (slower,
    # may lag behind the backend on newer agents, but functional without
    # Docker).
    info "Backend not running — using local detection (start backend for full info)."
    echo

    load_config

    step "Agents (local detection)"
    detect_agents
    show_detected_agents || warn "No agent detected"

    step "Repositories ($SCAN_PATH)"
    scan_repos "$SCAN_PATH"

    if [[ ${#REPO_PATHS[@]} -eq 0 ]]; then
        warn "No repository found"
        return
    fi

    for i in "${!REPO_NAMES[@]}"; do
        local name="${REPO_NAMES[$i]}"
        local status="${REPO_STATUS[$i]}"
        local color="$DIM"
        case "$status" in
            Validated*) color="$GREEN" ;;
            Audited*)   color="$GREEN" ;;
            Template*)  color="$YELLOW" ;;
            *MCP*)      color="$CYAN" ;;
        esac
        printf "  %s  ${color}%s${RESET}\n" "$name" "$status"
    done
}

cmd_agents() {
    # Dedicated "just show me the agents" command — API first, local fallback.
    if kronn_api_available; then
        kronn_api_show_agents
    else
        detect_agents
        show_detected_agents || warn "No agent detected"
        printf "\n  ${DIM}Tip: start the backend for full info (Ollama health, runtime probes): kronn start${RESET}\n"
    fi
}

cmd_projects() {
    # List projects — API only (projects live in the DB, no local fallback).
    if ! kronn_api_available; then
        warn "Backend not running. Start it first: kronn start"
        return 1
    fi
    local json
    json=$(kronn_api_data GET /projects) || { fail "Failed to query projects API"; return 1; }
    if command -v jq >/dev/null 2>&1; then
        local count
        count=$(echo "$json" | jq 'length')
        info "$count project(s):"
        echo "$json" | jq -r '.[] | "  \(.name)  \u001b[2m\(.path)\u001b[0m  [\(.audit_status)]"'
    elif command -v python3 >/dev/null 2>&1; then
        echo "$json" | python3 -c "
import sys, json
D='\033[2m'; R='\033[0m'
projects = json.load(sys.stdin)
print(f'  {len(projects)} project(s):')
for p in projects:
    print(f'  {p[\"name\"]}  {D}{p[\"path\"]}{R}  [{p.get(\"audit_status\",\"?\")}]')
"
    else
        echo "$json"
    fi
}

cmd_init() {
    local target="${1:-.}"
    target=$(cd "$target" 2>/dev/null && pwd)

    if [[ ! -d "$target/.git" ]]; then
        fail "$target is not a git repository."
        return 1
    fi

    init_repo "$target"
}

cmd_stop() {
    step "Stopping Kronn"
    (cd "$KRONN_DIR" && make stop)
    success "Kronn stopped."
}

cmd_restart() {
    cmd_stop
    echo
    cmd_web
}

cmd_logs() {
    # Logs live in the Docker containers' stdout — no file on disk. This
    # wrapper calls `docker compose logs -f` so the user gets a live tail
    # from all services (backend, frontend, gateway). Ctrl+C detaches.
    printf "  ${DIM}Streaming logs (Ctrl+C to detach — containers keep running).${RESET}\n"
    printf "  ${DIM}Grep helpers: 'kronn::agent_detect' | 'kronn::scanner'${RESET}\n"
    echo ""
    (cd "$KRONN_DIR" && make logs)
}

cmd_doctor() {
    # Diagnostic command — surfaces the kind of host-state corruption
    # that silently breaks Kronn-mounted bind dirs (TD-20260429).
    # Today checks:
    #   1. Root-owned files under host caches the container shares
    #      (~/.cache/uv, ~/.local/share/rtk, etc.) — leftovers from
    #      pre-APP_UID upgrades that break host `uvx` / `rtk gain`.
    #   2. Host runtime tooling — `uvx`, `glab` (≥ 1.59), `npx` —
    #      required by Kronn-managed MCP servers when invoked from
    #      the host CLI.
    #   3. Docker presence + version — Kronn won't run without it.
    banner
    local total_issues=0

    # ─── 1. Host cache permissions (uid-0 detection) ─────────────────
    step "Host cache permissions"
    # Search up to depth 4 — enough to catch the typical
    # `~/.cache/uv/CACHEDIR.TAG` and `~/.local/share/rtk/<dir>/<file>`
    # offenders without scanning the entire user home tree (slow on
    # a populated $HOME).
    local cache_paths=("$HOME/.cache" "$HOME/.local/share")
    local me_uid; me_uid=$(id -u)
    local cache_issues=0
    for path in "${cache_paths[@]}"; do
        if [[ ! -d "$path" ]]; then continue; fi
        # `find -uid` is portable across Linux/macOS BSD find.
        local offenders
        offenders=$(find "$path" -maxdepth 4 -uid 0 2>/dev/null | head -10)
        if [[ -n "$offenders" ]]; then
            warn "Root-owned entries under $path:"
            echo "$offenders" | sed 's/^/      /'
            cache_issues=$((cache_issues + 1))
        fi
    done
    if [[ $cache_issues -eq 0 ]]; then
        success "No root-owned entries detected in host caches"
    else
        echo
        info "Pre-APP_UID Kronn (≤ 0.5.x) ran as root and left these behind."
        info "Fix (any of):"
        printf "    ${DIM}sudo chown -R %s:%s <path>${RESET}                  ${DIM}# keep + reclaim${RESET}\n" "$me_uid" "$(id -g)"
        printf "    ${DIM}sudo rm -rf <path>${RESET}                              ${DIM}# wipe — re-created clean on next use${RESET}\n"
        total_issues=$((total_issues + cache_issues))
    fi

    # ─── 2. Host runtime tooling ─────────────────────────────────────
    step "Host runtime prerequisites"
    local tooling_issues=0
    # uvx — Python MCP servers (atlassian, cloudwatch, docker, git, …)
    if command -v uvx >/dev/null 2>&1; then
        success "uvx — $(uvx --version 2>/dev/null | head -1)"
    else
        warn "uvx — not found in PATH (Python-based MCPs will fail from host CLI)"
        printf "    ${DIM}Install: curl -LsSf https://astral.sh/uv/install.sh | sh${RESET}\n"
        tooling_issues=$((tooling_issues + 1))
    fi
    # glab — GitLab MCP integration. ≥ 1.59 required for the agent-friendly
    # `glab mcp serve` subcommand (added in 1.59).
    if command -v glab >/dev/null 2>&1; then
        local glab_v
        glab_v=$(glab --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "")
        if [[ -n "$glab_v" ]]; then
            # Lexicographic compare on dotted-version is wrong (1.10 < 1.9).
            # Use sort -V to pick the lowest of {observed, 1.59.0}.
            local lowest
            lowest=$(printf '%s\n%s\n' "$glab_v" "1.59.0" | sort -V | head -1)
            if [[ "$lowest" == "1.59.0" ]]; then
                success "glab — $glab_v (≥ 1.59 required)"
            else
                warn "glab — $glab_v (< 1.59 required for 'glab mcp serve' — please upgrade)"
                tooling_issues=$((tooling_issues + 1))
            fi
        else
            warn "glab — installed but version not parseable"
        fi
    else
        info "glab — not found in PATH (only needed if you use the GitLab MCP)"
    fi
    # npx — npm-based MCP servers (rate-limited at first use, but always works).
    if command -v npx >/dev/null 2>&1; then
        success "npx — $(npx --version 2>/dev/null | head -1)"
    else
        warn "npx — not found in PATH (npm-based MCPs will fail from host CLI)"
        printf "    ${DIM}Install Node.js to get npx: https://nodejs.org${RESET}\n"
        tooling_issues=$((tooling_issues + 1))
    fi
    total_issues=$((total_issues + tooling_issues))

    # ─── 3. Docker ──────────────────────────────────────────────────
    step "Docker"
    if command -v docker >/dev/null 2>&1; then
        local docker_v
        docker_v=$(docker --version 2>/dev/null | head -1)
        success "${docker_v:-Docker installed}"
        if docker info >/dev/null 2>&1; then
            success "Docker daemon reachable"
        else
            warn "Docker daemon not reachable — start Docker Desktop / 'sudo systemctl start docker'"
            total_issues=$((total_issues + 1))
        fi
    else
        warn "Docker not found — Kronn requires Docker to run"
        printf "    ${DIM}Install Docker Desktop or your platform's docker-engine package${RESET}\n"
        total_issues=$((total_issues + 1))
    fi

    # ─── Summary ─────────────────────────────────────────────────────
    echo
    if [[ $total_issues -eq 0 ]]; then
        success "All checks passed — host environment is healthy."
    else
        warn "$total_issues issue(s) found — see above for fix instructions."
        info "Re-run 'kronn doctor' after applying fixes to confirm."
        return 1
    fi
}

cmd_help() {
    banner
    printf "${BOLD}Usage:${RESET} kronn [command]\n"
    echo
    printf "  ${CYAN}start${RESET}          Interactive flow (default)\n"
    printf "  ${CYAN}stop${RESET}           Stop services\n"
    printf "  ${CYAN}restart${RESET}        Restart services\n"
    printf "  ${CYAN}logs${RESET}           Show logs\n"
    printf "  ${CYAN}status${RESET}         Overview (auto-delegates to API when backend is up)\n"
    printf "  ${CYAN}doctor${RESET}         Diagnose host-environment issues (uid-0 caches, missing CLIs, Docker)\n"
    printf "  ${CYAN}agents${RESET}         List detected agents\n"
    printf "  ${CYAN}projects${RESET}       List projects (requires backend)\n"
    printf "  ${CYAN}init [path]${RESET}    Configure a repository\n"
    printf "  ${CYAN}mcp sync${RESET}       Sync .mcp.json files\n"
    printf "  ${CYAN}mcp check${RESET}      Check MCP prerequisites\n"
    printf "  ${CYAN}web${RESET}            Launch web interface directly\n"
    printf "  ${CYAN}mcp secrets${RESET}    Configure tokens\n"
    printf "  ${CYAN}help${RESET}           This help\n"
    echo
    printf "${BOLD}Flags (any command):${RESET}\n"
    printf "  ${CYAN}--debug${RESET}        Force verbose backend logs for this run\n"
    printf "                 (otherwise the backend reads config.server.debug_mode)\n"
    printf "                 With ${BOLD}start${RESET}: auto-tails the logs after boot\n"
    echo
    printf "${BOLD}Where are the logs?${RESET}\n"
    printf "  Logs live on the containers' stdout — no file on disk.\n"
    printf "  ${CYAN}kronn logs${RESET}    Live tail of all services (Ctrl+C detaches)\n"
    printf "  ${CYAN}kronn logs${RESET} ${DIM}| grep 'kronn::agent_detect'${RESET}   ${DIM}Agent detection trace${RESET}\n"
    printf "  ${CYAN}kronn logs${RESET} ${DIM}| grep 'kronn::scanner'${RESET}        ${DIM}Project scan + host-path mapping${RESET}\n"
    echo
}

# ─── Utility ─────────────────────────────────────────────────────────────────

install_make() {
    if command -v apt >/dev/null 2>&1; then
        sudo apt update && sudo apt install -y make
    elif command -v dnf >/dev/null 2>&1; then
        sudo dnf install -y make
    elif command -v pacman >/dev/null 2>&1; then
        sudo pacman -S --noconfirm make
    elif command -v brew >/dev/null 2>&1; then
        brew install make
    else
        fail "Unable to detect the package manager."
        echo "Install make manually."
        return 1
    fi
}

# ─── Self-install ────────────────────────────────────────────────────────────

ensure_in_path() {
    local link_dir="$HOME/.local/bin"
    local link_path="$link_dir/kronn"

    # Already accessible as `kronn`?
    local current
    current=$(command -v kronn 2>/dev/null || true)
    if [[ -n "$current" ]]; then
        # Resolve symlink to check it points here
        local resolved
        resolved=$(readlink -f "$current" 2>/dev/null || echo "$current")
        if [[ "$resolved" == "$KRONN_DIR/kronn" ]]; then
            return 0
        fi
    fi

    echo
    info "kronn is not in your PATH."
    if ask_yn "Create a symlink in $link_dir?"; then
        mkdir -p "$link_dir"
        ln -sf "$KRONN_DIR/kronn" "$link_path"
        success "Symlink created: $link_path -> $KRONN_DIR/kronn"

        # Check if ~/.local/bin is in PATH
        if [[ ":$PATH:" != *":$link_dir:"* ]]; then
            warn "$link_dir is not in your PATH."
            printf "  ${DIM}Add to ~/.bashrc or ~/.zshrc:${RESET}\n"
            printf "  ${DIM}  export PATH=\"\$HOME/.local/bin:\$PATH\"${RESET}\n"
            echo
            # Add it for this session
            export PATH="$link_dir:$PATH"
        fi

        success "kronn is now accessible everywhere."

        # Install shell completions
        local comp_dir="$KRONN_DIR/completions"
        if [[ -n "$BASH_VERSION" ]] && [[ -f "$comp_dir/kronn.bash" ]]; then
            local bash_comp_dir="${XDG_DATA_HOME:-$HOME/.local/share}/bash-completion/completions"
            mkdir -p "$bash_comp_dir"
            ln -sf "$comp_dir/kronn.bash" "$bash_comp_dir/kronn"
            info "Bash completions installed (restart shell to activate)"
        elif [[ -n "$ZSH_VERSION" ]] && [[ -f "$comp_dir/kronn.zsh" ]]; then
            local zsh_comp_dir="${HOME}/.zsh/completions"
            mkdir -p "$zsh_comp_dir"
            ln -sf "$comp_dir/kronn.zsh" "$zsh_comp_dir/_kronn"
            if [[ ":$FPATH:" != *":$zsh_comp_dir:"* ]]; then
                info "Zsh completions installed. Add to ~/.zshrc:"
                printf "  ${DIM}  fpath=(~/.zsh/completions \$fpath)${RESET}\n"
                printf "  ${DIM}  autoload -Uz compinit && compinit${RESET}\n"
            else
                info "Zsh completions installed (restart shell to activate)"
            fi
        fi
    fi
}

# ─── Main ────────────────────────────────────────────────────────────────────

main() {
    load_config

    # ── Global flags (pre-scan before routing to a subcommand) ──────────────
    # Today we only recognise `--debug` which sets KRONN_DEBUG=1 for the rest
    # of the script (read by cmd_web → make start). Add more here if needed,
    # but keep them truly global — per-subcommand flags belong in the
    # subcommand's own parsing.
    local remaining=()
    for arg in "$@"; do
        case "$arg" in
            --debug) export KRONN_DEBUG=1 ;;
            *) remaining+=("$arg") ;;
        esac
    done
    set -- "${remaining[@]+"${remaining[@]}"}"

    local cmd="${1:-start}"
    shift 2>/dev/null || true

    case "$cmd" in
        start)
            ensure_in_path
            cmd_start
            ;;
        stop)        cmd_stop ;;
        restart)     cmd_restart ;;
        logs)        cmd_logs ;;
        status)      cmd_status ;;
        doctor)      cmd_doctor ;;
        agents)      cmd_agents ;;
        projects)    cmd_projects ;;
        init)        cmd_init "$@" ;;
        mcp)         cmd_mcp "$@" ;;
        web)         cmd_web ;;
        help|--help|-h) cmd_help ;;
        *)
            fail "Unknown command: $cmd"
            cmd_help
            exit 1
            ;;
    esac
}

main "$@"
