#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────
# jupyter-hub — Cross-project Jupyter instance manager
#
# Discovers and manages ALL JupyterLab instances across your projects.
# Prevents port conflicts, finds orphan processes, provides unified
# status and stop-all functionality.
#
# Part of: https://github.com/saymrwulf/JupyterManager
#
# Usage:
#   jupyter-hub status          Show all running Jupyter instances
#   jupyter-hub stop-all        Stop all running Jupyter instances
#   jupyter-hub ports           Show port allocation across 8888-8899
#   jupyter-hub orphans         Find orphan Jupyter processes
#   jupyter-hub kill-orphans    Kill orphan Jupyter processes
#   jupyter-hub which PORT      Show which project owns a port
#   jupyter-hub config          Show current configuration
# ──────────────────────────────────────────────────────────────────────
set -euo pipefail

JHUB_VERSION="1.0.0"
JHUB_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
JHUB_CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/jupyter-hub"
JHUB_CONFIG_FILE="$JHUB_CONFIG_DIR/config.sh"

# ── Colours ───────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# ── Default configuration ────────────────────────────────────────────
# Override in $JHUB_CONFIG_FILE
PORT_RANGE_START=8888
PORT_RANGE_END=8899
SCAN_DIRS=()

# ── Load configuration ───────────────────────────────────────────────

_load_config() {
    # Load project-shipped defaults
    if [[ -f "$JHUB_ROOT/config/defaults.sh" ]]; then
        source "$JHUB_ROOT/config/defaults.sh"
    fi

    # Load user overrides
    if [[ -f "$JHUB_CONFIG_FILE" ]]; then
        source "$JHUB_CONFIG_FILE"
    fi

    # Fallback if no scan dirs configured
    if [[ ${#SCAN_DIRS[@]} -eq 0 ]]; then
        SCAN_DIRS=(
            "$HOME/GitClone/ClaudeCodeProjects"
            "$HOME/GitClone/CodexProjects"
        )
    fi
}

# ── Discovery ─────────────────────────────────────────────────────────

_discover_projects() {
    local projects=()
    for scan_dir in "${SCAN_DIRS[@]}"; do
        if [[ -d "$scan_dir" ]]; then
            while IFS= read -r -d '' app_sh; do
                local project_dir
                project_dir="$(dirname "$(dirname "$app_sh")")"
                # Verify it's a Jupyter-capable project
                if [[ -d "$project_dir/notebooks" ]] || grep -q "jupyter" "$project_dir/pyproject.toml" 2>/dev/null; then
                    projects+=("$project_dir")
                fi
            done < <(find "$scan_dir" -maxdepth 3 -path "*/scripts/app.sh" -print0 2>/dev/null)
        fi
    done
    printf '%s\n' "${projects[@]}" | sort -u
}

_project_name() {
    basename "$1"
}

_pid_alive() {
    kill -0 "$1" 2>/dev/null
}

_find_server_info() {
    local project_dir="$1"

    # Strategy 1: .logs/jupyter.pid
    local pid_file="$project_dir/.logs/jupyter.pid"
    if [[ -f "$pid_file" ]]; then
        local pid
        pid=$(cat "$pid_file")
        if [[ -n "$pid" ]] && _pid_alive "$pid"; then
            echo "pid=$pid source=pid_file"
            return 0
        fi
    fi

    # Strategy 2: .run/jupyter.pid
    pid_file="$project_dir/.run/jupyter.pid"
    if [[ -f "$pid_file" ]]; then
        local pid
        pid=$(cat "$pid_file")
        if [[ -n "$pid" ]] && _pid_alive "$pid"; then
            echo "pid=$pid source=run_pid"
            return 0
        fi
    fi

    # Strategy 3: Jupyter runtime JSON files
    for runtime_dir in "$project_dir/.jupyter_runtime" "$project_dir/.run"; do
        if [[ -d "$runtime_dir" ]]; then
            for json_file in "$runtime_dir"/jpserver-*.json; do
                if [[ -f "$json_file" ]]; then
                    local pid port token
                    pid=$(python3 -c "import json; d=json.load(open('$json_file')); print(d.get('pid',''))" 2>/dev/null || true)
                    port=$(python3 -c "import json; d=json.load(open('$json_file')); print(d.get('port',''))" 2>/dev/null || true)
                    token=$(python3 -c "import json; d=json.load(open('$json_file')); print(d.get('token',''))" 2>/dev/null || true)
                    if [[ -n "$pid" ]] && _pid_alive "$pid"; then
                        echo "pid=$pid port=$port token=$token source=runtime_json"
                        return 0
                    fi
                fi
            done
        fi
    done

    return 1
}

_pid_port() {
    local pid="$1"
    lsof -Pan -p "$pid" -i TCP -sTCP:LISTEN 2>/dev/null | awk 'NR>1 {
        split($9, a, ":"); print a[length(a)]
    }' | head -1
}

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

cmd_status() {
    echo ""
    echo -e "  ${BOLD}Jupyter Hub${NC} v${JHUB_VERSION}"
    echo -e "  ${DIM}$(date '+%Y-%m-%d %H:%M:%S')${NC}"
    echo ""

    local found_any=false

    while IFS= read -r project_dir; do
        [[ -z "$project_dir" ]] && continue
        local name
        name=$(_project_name "$project_dir")

        local info
        if info=$(_find_server_info "$project_dir"); then
            found_any=true
            local pid port
            pid=$(echo "$info" | grep -o 'pid=[0-9]*' | cut -d= -f2)
            port=$(_pid_port "$pid" 2>/dev/null || echo "?")
            echo -e "  ${GREEN}●${NC} ${BOLD}$name${NC}"
            echo -e "    PID: $pid  Port: $port"
            echo -e "    Dir: ${DIM}$project_dir${NC}"
            if [[ "$port" != "?" ]]; then
                echo -e "    URL: http://localhost:$port/lab"
            fi
        else
            echo -e "  ${DIM}○ $name${NC}  ${DIM}(stopped)${NC}"
        fi
    done < <(_discover_projects)

    if ! $found_any; then
        echo -e "  ${DIM}No running Jupyter instances found.${NC}"
    fi

    echo ""
}

cmd_ports() {
    echo ""
    echo -e "  ${BOLD}Port Allocation (${PORT_RANGE_START}-${PORT_RANGE_END})${NC}"
    echo ""

    for port in $(seq "$PORT_RANGE_START" "$PORT_RANGE_END"); do
        local pid
        pid=$(lsof -ti :"$port" 2>/dev/null | head -1 || true)
        if [[ -n "$pid" ]]; then
            local cmd_line project_name="unknown"
            cmd_line=$(ps -p "$pid" -o args= 2>/dev/null || true)
            if [[ "$cmd_line" =~ notebook-dir[=\ ]*([^ ]*) ]]; then
                project_name=$(basename "$(dirname "${BASH_REMATCH[1]}")")
            elif [[ "$cmd_line" =~ root_dir[=\ ]*([^ ]*) ]]; then
                project_name=$(basename "${BASH_REMATCH[1]}")
            fi
            echo -e "  ${RED}$port${NC}  pid:$pid  ${BOLD}$project_name${NC}"
            echo -e "        ${DIM}$(echo "$cmd_line" | head -c 100)${NC}"
        else
            echo -e "  ${GREEN}$port${NC}  ${DIM}free${NC}"
        fi
    done
    echo ""
}

cmd_stop_all() {
    echo ""
    local stopped=0

    while IFS= read -r project_dir; do
        [[ -z "$project_dir" ]] && continue
        local name
        name=$(_project_name "$project_dir")

        local info
        if info=$(_find_server_info "$project_dir"); then
            local pid
            pid=$(echo "$info" | grep -o 'pid=[0-9]*' | cut -d= -f2)

            if [[ -x "$project_dir/scripts/app.sh" ]]; then
                echo -e "  Stopping ${BOLD}$name${NC} via app.sh..."
                (cd "$project_dir" && bash scripts/app.sh stop 2>/dev/null) || true
            else
                echo -e "  Stopping ${BOLD}$name${NC} (pid $pid)..."
                kill -TERM "$pid" 2>/dev/null || true
            fi
            stopped=$((stopped + 1))
        fi
    done < <(_discover_projects)

    # Also kill any orphans not claimed by known projects
    local orphan_count=0
    while IFS= read -r pid; do
        [[ -z "$pid" ]] && continue
        # Skip PIDs we already stopped via app.sh
        local already_stopped=false
        while IFS= read -r project_dir; do
            [[ -z "$project_dir" ]] && continue
            for pf in "$project_dir/.logs/jupyter.pid" "$project_dir/.run/jupyter.pid"; do
                if [[ -f "$pf" ]] && [[ "$(cat "$pf" 2>/dev/null)" == "$pid" ]]; then
                    already_stopped=true
                    break 2
                fi
            done
        done < <(_discover_projects)

        if ! $already_stopped && _pid_alive "$pid"; then
            echo -e "  Stopping orphan pid $pid..."
            kill -TERM "$pid" 2>/dev/null || true
            orphan_count=$((orphan_count + 1))
        fi
    done < <(pgrep -f "jupyter.*lab" 2>/dev/null || true)

    local total=$((stopped + orphan_count))
    if (( total > 0 )); then
        echo ""
        echo -e "  ${GREEN}Stopped $total instance(s).${NC}"
    else
        echo -e "  ${DIM}No running instances to stop.${NC}"
    fi
    echo ""
}

cmd_orphans() {
    echo ""
    echo -e "  ${BOLD}Orphan Jupyter Processes${NC}"
    echo -e "  ${DIM}(Jupyter processes not tracked by any project's PID file)${NC}"
    echo ""

    local found=false
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        local pid
        pid=$(echo "$line" | awk '{print $2}')

        local claimed=false
        while IFS= read -r project_dir; do
            [[ -z "$project_dir" ]] && continue
            for pid_file in "$project_dir/.logs/jupyter.pid" "$project_dir/.run/jupyter.pid"; do
                if [[ -f "$pid_file" ]] && [[ "$(cat "$pid_file")" == "$pid" ]]; then
                    claimed=true
                    break 2
                fi
            done
            for json_file in "$project_dir/.jupyter_runtime"/jpserver-*.json; do
                if [[ -f "$json_file" ]]; then
                    local json_pid
                    json_pid=$(python3 -c "import json; print(json.load(open('$json_file')).get('pid',''))" 2>/dev/null || true)
                    if [[ "$json_pid" == "$pid" ]]; then
                        claimed=true
                        break 2
                    fi
                fi
            done
        done < <(_discover_projects)

        if ! $claimed; then
            found=true
            local port cmd_short
            port=$(_pid_port "$pid" 2>/dev/null || echo "?")
            cmd_short=$(ps -p "$pid" -o args= 2>/dev/null | head -c 100 || echo "?")
            echo -e "  ${YELLOW}!${NC} PID $pid  Port: $port"
            echo -e "    ${DIM}$cmd_short${NC}"
        fi
    done < <(ps aux | grep "[j]upyter.*lab" || true)

    if ! $found; then
        echo -e "  ${GREEN}No orphan processes found.${NC}"
    else
        echo ""
        echo "  To kill orphans: jupyter-hub kill-orphans"
    fi
    echo ""
}

cmd_kill_orphans() {
    local orphans=()
    while IFS= read -r line; do
        [[ -z "$line" ]] && continue
        local pid
        pid=$(echo "$line" | awk '{print $2}')

        local claimed=false
        while IFS= read -r project_dir; do
            [[ -z "$project_dir" ]] && continue
            for pid_file in "$project_dir/.logs/jupyter.pid" "$project_dir/.run/jupyter.pid"; do
                if [[ -f "$pid_file" ]] && [[ "$(cat "$pid_file")" == "$pid" ]]; then
                    claimed=true
                    break 2
                fi
            done
        done < <(_discover_projects)

        if ! $claimed; then
            orphans+=("$pid")
        fi
    done < <(ps aux | grep "[j]upyter.*lab" || true)

    if [[ ${#orphans[@]} -eq 0 ]]; then
        echo "No orphan Jupyter processes found."
        return 0
    fi

    for pid in "${orphans[@]}"; do
        echo "Killing orphan pid $pid..."
        kill -TERM "$pid" 2>/dev/null || true
    done
    echo "Killed ${#orphans[@]} orphan(s)."
}

cmd_which() {
    local port="${1:-}"
    if [[ -z "$port" ]]; then
        echo "Usage: jupyter-hub which PORT"
        exit 1
    fi

    local pid
    pid=$(lsof -ti :"$port" 2>/dev/null | head -1 || true)
    if [[ -z "$pid" ]]; then
        echo "Port $port is free."
        return 0
    fi

    echo "Port $port is used by pid $pid"
    local cmd_line
    cmd_line=$(ps -p "$pid" -o args= 2>/dev/null || echo "unknown")
    echo "Command: $cmd_line"

    while IFS= read -r project_dir; do
        [[ -z "$project_dir" ]] && continue
        local info
        if info=$(_find_server_info "$project_dir" 2>/dev/null); then
            local proj_pid
            proj_pid=$(echo "$info" | grep -o 'pid=[0-9]*' | cut -d= -f2)
            if [[ "$proj_pid" == "$pid" ]]; then
                echo "Project: $(_project_name "$project_dir")"
                echo "Path: $project_dir"
                return 0
            fi
        fi
    done < <(_discover_projects)

    echo "Project: (not matched to any known project)"
}

cmd_config() {
    echo ""
    echo -e "  ${BOLD}Jupyter Hub Configuration${NC}"
    echo ""
    echo "  Install root:  $JHUB_ROOT"
    echo "  Config dir:    $JHUB_CONFIG_DIR"
    echo "  Config file:   $JHUB_CONFIG_FILE"
    if [[ -f "$JHUB_CONFIG_FILE" ]]; then
        echo "  Config status: loaded"
    else
        echo "  Config status: using defaults"
    fi
    echo "  Port range:    ${PORT_RANGE_START}-${PORT_RANGE_END}"
    echo ""
    echo "  Scan directories:"
    for d in "${SCAN_DIRS[@]}"; do
        if [[ -d "$d" ]]; then
            echo -e "    ${GREEN}✓${NC} $d"
        else
            echo -e "    ${RED}✗${NC} $d (not found)"
        fi
    done
    echo ""
    echo "  Discovered projects:"
    while IFS= read -r project_dir; do
        [[ -z "$project_dir" ]] && continue
        echo "    $(_project_name "$project_dir")  →  $project_dir"
    done < <(_discover_projects)
    echo ""
}

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

_load_config

case "${1:-help}" in
    status)       cmd_status ;;
    ports)        cmd_ports ;;
    stop-all)     cmd_stop_all ;;
    orphans)      cmd_orphans ;;
    kill-orphans) cmd_kill_orphans ;;
    which)        cmd_which "${2:-}" ;;
    config)       cmd_config ;;
    version|--version|-v)
        echo "jupyter-hub v${JHUB_VERSION}"
        ;;
    help|--help|-h)
        echo ""
        echo -e "  ${BOLD}jupyter-hub${NC} v${JHUB_VERSION} — cross-project Jupyter manager"
        echo ""
        echo "  Commands:"
        echo "    status           Show all running Jupyter instances"
        echo "    stop-all         Stop all running instances"
        echo "    ports            Show port allocation (${PORT_RANGE_START}-${PORT_RANGE_END})"
        echo "    orphans          Find orphan Jupyter processes"
        echo "    kill-orphans     Kill orphan processes"
        echo "    which PORT       Show which project owns a port"
        echo "    config           Show current configuration"
        echo "    version          Show version"
        echo ""
        echo "  Configuration:"
        echo "    $JHUB_CONFIG_FILE"
        echo ""
        ;;
    *)
        echo "Unknown command: $1"
        echo "Run 'jupyter-hub help' for usage."
        exit 1
        ;;
esac
