#!/bin/bash
# o — unified CLI for the ollie 9P namespace.
#
# Path resolution:
#   Paths are resolved based on context:
#   - Absolute paths (starting with session/, agents, models, etc.) → used as-is
#   - Relative paths with $session set but no $agent → session/$session/<path>
#   - Relative paths with both $session and $agent → session/$session/agent/$agent/<path>
#   - No context set → path used as-is (root-relative)
#
# Subcommands:
#   read   <path>         Read a file
#   write  <path> [data]  Write to a file (args or stdin)
#   readloop <path>       Read in a loop (re-reads after each return)
#   ls     [path]         List namespace entries
#   ctl    <command>      Send a control command (stop, compact, kill, ...)
#   prompt                Interactive REPL for submitting prompts (requires agent)
#   tail                  Tail chat output (requires agent)
#   tui                   Launch tmux TUI (requires agent)
#   env                   Print resolved session/agent environment
#   stop                  Interrupt running agent (requires agent)
#   kill                  Kill session (requires session)
#
# Environment:
#   session               Session name (set via 'eval $(o env)')
#   agent                 Agent name

set -euo pipefail

# ── Path classification ─────────────────────────────────────────

# Root-level entries (no context needed)
ROOT_ENTRIES="session elevate agents backends models help ctl eventwait complete generate route aliases tools"

# Session-level files (exist directly under session/{name}/)
SESSION_FILES="env paused connected ctl name id elevate"

# Agent-level files (exist under session/{name}/agent/{name}/)
AGENT_FILES="prompt prompt.prev fifo.in fifo.out chat connection state statewait log offset plan cfg ctl cwd usage cost ctxsz models systemprompt context tail tools name id proc"

is_root_path() {
    local path="$1"
    local first="${path%%/*}"
    for entry in $ROOT_ENTRIES; do
        [[ "$first" == "$entry" ]] && return 0
    done
    return 1
}

is_session_file() {
    local name="$1"
    for f in $SESSION_FILES; do
        [[ "$name" == "$f" ]] && return 0
    done
    [[ "$name" == "agent" || "$name" == agent/* ]] && return 0
    return 1
}

is_agent_file() {
    local name="$1"
    local base="${name%%/*}"
    for f in $AGENT_FILES; do
        [[ "$base" == "$f" ]] && return 0
    done
    return 1
}

# ── Path resolution with validation ─────────────────────────────

# Resolve a path based on current context, with meaningful errors.
# Returns 0 on success (path on stdout), 1 on error (message on stderr).
resolve_path() {
    local path="$1"
    
    # Root-level paths used as-is
    if is_root_path "$path"; then
        echo "$path"
        return 0
    fi
    
    # Get context
    local s="${session:-}"
    local a="${agent:-}"
    
    # Determine what level this path requires
    local base="${path%%/*}"
    
    # Check if it's an agent-level file
    if is_agent_file "$base"; then
        if [[ -z "$s" ]]; then
            echo "error: '$path' requires session context" >&2
            echo "  export session=<name>  or  use full path: session/<name>/agent/<agent>/$path" >&2
            return 1
        fi
        if [[ -z "$a" ]]; then
            echo "error: '$path' requires agent context" >&2
            echo "  export agent=<name>  or  use full path: session/$s/agent/<name>/$path" >&2
            return 1
        fi
        echo "session/$s/agent/$a/$path"
        return 0
    fi
    
    # Check if it's a session-level file/dir
    if is_session_file "$base"; then
        if [[ -z "$s" ]]; then
            echo "error: '$path' requires session context" >&2
            echo "  export session=<name>  or  use full path: session/<name>/$path" >&2
            return 1
        fi
        echo "session/$s/$path"
        return 0
    fi
    
    # Unknown relative path — try with whatever context we have
    if [[ -n "$s" && -n "$a" ]]; then
        echo "session/$s/agent/$a/$path"
    elif [[ -n "$s" ]]; then
        echo "session/$s/$path"
    else
        echo "$path"
    fi
    return 0
}

# ── Context requirements for specific commands ──────────────────

require_session() {
    local s="${session:-}"
    if [[ -z "$s" ]]; then
        echo "error: no session set" >&2
        echo "  export session=<name>" >&2
        return 1
    fi
    echo "$s"
}

require_agent() {
    local s="$1"
    local a="${agent:-}"
    if [[ -z "$a" ]]; then
        echo "error: no agent set" >&2
        echo "  export agent=<name>" >&2
        return 1
    fi
    echo "$a"
}

agent_path() {
    local s a
    s=$(require_session) || return 1
    a=$(require_agent "$s") || return 1
    echo "session/$s/agent/$a"
}

# ── Subcommands ──────────────────────────────────────────────────

cmd_read() {
    if [[ $# -lt 1 ]]; then
        echo "usage: o read <path>" >&2
        exit 1
    fi
    local path
    path=$(resolve_path "$1") || exit 1
    ollie-9p read "$path"
}

cmd_write() {
    if [[ $# -lt 1 ]]; then
        echo "usage: o write <path> [data...]" >&2
        exit 1
    fi
    local file="$1"; shift
    local path
    path=$(resolve_path "$file") || exit 1
    if [[ $# -gt 0 ]]; then
        printf '%s\n' "$*" | ollie-9p write "$path"
    else
        ollie-9p write "$path"
    fi
}

cmd_ls() {
    if [[ $# -ge 1 ]]; then
        local path
        path=$(resolve_path "$1") || exit 1
        ollie-9p ls "$path"
    else
        # No arg: list based on context level
        local s="${session:-}"
        local a="${agent:-}"
        
        if [[ -n "$s" && -n "$a" ]]; then
            ollie-9p ls "session/$s/agent/$a"
        elif [[ -n "$s" ]]; then
            ollie-9p ls "session/$s"
        else
            ollie-9p ls ""
        fi
    fi
}

cmd_generate() {
    # One-shot generation via /generate (no session/agent needed)
    # Usage: o generate "prompt"
    #        echo "prompt" | o generate
    #        o generate -1 "prompt"  (same as above)
    local prompt=""
    
    # Handle -1 flag (ignored, for compatibility)
    [[ "${1:-}" == "-1" ]] && shift
    
    if [[ $# -gt 0 ]]; then
        prompt="$*"
    else
        prompt=$(cat)
    fi
    
    [[ -z "$prompt" ]] && { echo "usage: o generate <prompt>" >&2; exit 1; }
    
    printf '%s' "$prompt" | ollie-9p rdwr generate
}

cmd_readloop() {
    if [[ $# -lt 1 ]]; then
        echo "usage: o readloop <path>" >&2
        exit 1
    fi
    local file="$1"; shift
    local path
    path=$(resolve_path "$file") || exit 1
    while true; do
        ollie-9p read "$path" || break
    done
}

cmd_ctl() {
    if [[ $# -lt 1 ]]; then
        echo "usage: o ctl <command> [args...]" >&2
        echo "  commands: stop, compact, kill, clear, model <m>, backend <b>, cwd <d>" >&2
        exit 1
    fi
    local path
    path=$(agent_path) || exit 1
    echo "$*" | ollie-9p write "$path/ctl"
}

cmd_prompt() {
    local s a path
    s=$(require_session) || exit 1
    a=$(require_agent "$s") || exit 1
    path="session/$s/agent/$a"
    
    echo "[$s/$a]"
    echo "Type . on a blank line to send, Ctrl+D to exit."
    echo "Slash commands: /stop /compact /kill /clear /model /backend /cwd /help"
    echo ""

    local buf="" line
    while true; do
        local prompt_str
        if [[ -z "$buf" ]]; then
            prompt_str=$'\001\033[1m\002>\001\033[0m\002 '
        else
            prompt_str="  "
        fi
        if ! IFS= read -e -r -p "$prompt_str" line; then
            if [[ -n "$buf" ]]; then
                history -s "$buf"
                printf '%s\n' "$buf" | ollie-9p write "$path/prompt"
            fi
            printf "\n"
            break
        fi
        if [[ -z "$buf" && "$line" == /* ]]; then
            history -s "$line"
            local cmd="${line#/}"
            case "$cmd" in
                stop)    echo "stop" | ollie-9p write "$path/ctl"; echo "[stopped]" ;;
                compact) echo "compact" | ollie-9p write "$path/ctl"; echo "[compacted]" ;;
                kill)    echo "kill" | ollie-9p write "session/$s/ctl"; echo "[session killed]"; break ;;
                clear)   echo "clear" | ollie-9p write "$path/ctl"; echo "[cleared]" ;;
                model*)  echo "$cmd" | ollie-9p write "$path/ctl"; echo "[${cmd}]" ;;
                backend*) echo "$cmd" | ollie-9p write "$path/ctl"; echo "[${cmd}]" ;;
                cwd*)    echo "$cmd" | ollie-9p write "$path/ctl"; echo "[${cmd}]" ;;
                quit|q)  tmux kill-session -t "o-${s}" 2>/dev/null; exit 0 ;;
                help)
                    echo "  /stop      — interrupt running agent"
                    echo "  /compact   — compact context"
                    echo "  /kill      — kill the session"
                    echo "  /clear     — clear history"
                    echo "  /model X   — switch model"
                    echo "  /backend X — switch backend"
                    echo "  /cwd X     — change working directory"
                    echo "  /quit /q   — kill the tmux TUI"
                    ;;
                *)  echo "unknown command: /$cmd (try /help)" ;;
            esac
            continue
        fi
        if [[ "$line" == "." ]]; then
            # Send prompt on "." alone
            if [[ -n "$buf" ]]; then
                history -s "$buf"
                printf '%s\n' "$buf" | ollie-9p write "$path/prompt"
                buf=""
            fi
            continue
        fi
        if [[ -z "$line" && -n "$buf" ]]; then
            # Empty line during multi-line input: append newline
            buf="$buf"$'\n'
        elif [[ -z "$line" ]]; then
            continue
        else
            if [[ -n "$buf" ]]; then
                buf="$buf"$'\n'"$line"
            else
                buf="$line"
            fi
        fi
    done
}

cmd_tail() {
    local path
    path=$(agent_path) || exit 1
    exec ollie-9p read "$path/chat"
}

cmd_env() {
    # Accept optional positional args to set context
    if [[ $# -ge 1 ]]; then
        echo "export session=$1"
        shift
    elif [[ -n "${session:-}" ]]; then
        echo "export session=$session"
    fi
    if [[ $# -ge 1 ]]; then
        echo "export agent=$1"
    elif [[ -n "${agent:-}" ]]; then
        echo "export agent=$agent"
    fi
}

cmd_stop() {
    local path
    path=$(agent_path) || exit 1
    echo "stop" | ollie-9p write "$path/ctl"
}

cmd_kill() {
    local s
    s=$(require_session) || exit 1
    echo "kill" | ollie-9p write "session/$s/ctl"
}

cmd_tui() {
    local s a
    s=$(require_session) || exit 1
    a=$(require_agent "$s") || exit 1

    local tmux_session="o-${s}"

    if [[ "${1:-}" == "exit" ]]; then
        tmux kill-session -t "$tmux_session" 2>/dev/null && echo "[killed $tmux_session]" || echo "no tui session: $tmux_session"
        return
    fi

    tmux kill-session -t "$tmux_session" 2>/dev/null || true

    # Layout:
    # ┌──────────────────────────────────┐
    # │  chat                            │
    # │                                  │
    # ├──────────────────────────────────┤
    # │  prompt (20%)                    │
    # └──────────────────────────────────┘
    # State shown in tmux status-right.

    # Pane 0: chat (top, 80%)
    tmux new-session -d -s "$tmux_session" -c "$HOME" \
        -e "session=$s" -e "agent=$a"
    tmux send-keys -t "$tmux_session" "clear; o tail | grep -v '\\[\\[\\[.*\\]\\]\\]'" Enter

    # Pane 1: prompt REPL (bottom, 20% height)
    tmux split-window -t "$tmux_session" -v -l 20% \
        -e "session=$s" -e "agent=$a"
    tmux send-keys -t "$tmux_session" "o prompt" Enter

    # Show agent state in tmux status bar (live updates via statewait)
    tmux set-option -t "$tmux_session" status-right "starting... | %H:%M"
    tmux set-option -t "$tmux_session" status-right-length 64
    # Background: loops on statewait, updates status bar instantly on each change
    while true; do
        ollie-9p read "session/$s/agent/$a/statewait" 2>/dev/null | while read -r state; do
            bg=""
            case "$state" in
                idle)    bg="colour70" ;;    # green
                calling*) bg="colour214" ;;  # orange
                thinking) bg="colour33" ;;   # blue
                paused)  bg="colour245" ;;   # gray
                *)       bg="default" ;;
            esac
            tmux set-option -t "$tmux_session" status-right-style "fg=default,bg=$bg"
            tmux set-option -t "$tmux_session" status-right "$state | %H:%M"
        done
    done &

    # Focus the prompt pane
    tmux select-pane -t "$tmux_session:0.1"

    exec tmux attach -t "$tmux_session"
}

cmd_help() {
    cat <<'EOF'
usage: o <command> [args...]

Path resolution:
  - Root paths (session/*, agents, models, etc.) → used as-is
  - Agent files (chat, state, prompt, etc.)      → require $session + $agent
  - Session files (env, ctl, name, etc.)         → require $session

Commands:
  read <path>          Read a file
  write <path> [data]  Write to a file (args or stdin)
  readloop <path>      Read in a loop (re-reads after return)
  ls [path]            List namespace entries

  generate <prompt>    One-shot generation (no session needed)

  ctl <cmd>            Send control command to agent
  stop                 Interrupt running agent
  kill                 Kill active session

  prompt               Interactive multi-line prompt REPL
  tail                 Stream chat output
  tui                  Launch tmux layout
  tui exit             Kill tmux TUI session

  env [session] [agent]  Print export statements for context
  help                   Show this help

Environment:
  session              Session name
  agent                Agent name

Examples:
  o ls                           # list at current context level
  o ls session                   # list all sessions
  o read models                  # read root-level file
  o generate "explain monads"    # one-shot, no session
  export session=foo agent=bar   # set context
  o read chat                    # read agent's chat
  o readloop statewait           # blocks until agent state changes
EOF
}

# ── Dispatch ─────────────────────────────────────────────────────

if [[ $# -lt 1 ]]; then
    cmd_help
    exit 0
fi

subcmd="$1"; shift
case "$subcmd" in
    read)     cmd_read "$@" ;;
    write)    cmd_write "$@" ;;
    readloop) cmd_readloop "$@" ;;
    ls)       cmd_ls "$@" ;;
    generate) cmd_generate "$@" ;;
    ctl)      cmd_ctl "$@" ;;
    stop)     cmd_stop "$@" ;;
    kill)     cmd_kill "$@" ;;
    prompt)   cmd_prompt "$@" ;;
    tail)     cmd_tail "$@" ;;
    tui)      cmd_tui "$@" ;;
    env)      cmd_env "$@" ;;
    help|-h|--help) cmd_help ;;
    *)
        echo "unknown command: $subcmd" >&2
        echo "run 'o help' for usage" >&2
        exit 1
        ;;
esac
