#!/bin/bash
# devdesk — persistent Claude Code sessions in tmux, shared by the browser UI and SSH.
#
#   devdesk attach [NAME]        attach to session NAME, creating it if needed; no NAME opens the picker
#   devdesk new NAME [PROJECT]   create session NAME (detached) in PROJECT's folder, or the default folder
#   devdesk rename OLD NEW       rename a session (running or dormant)
#   devdesk end NAME             stop session NAME and forget it
#   devdesk ls                   list sessions
#   devdesk pick                 interactive picker
#
# The web UI calls `devdesk attach --list | --new NAME [PROJECT] | --rename OLD NEW | --end NAME`
# through ttyd; those print JSON.
set -euo pipefail

# shellcheck source=libexec/devdesk-lib.sh
. "${DEVDESK_LIBEXEC:-/usr/lib/devdesk}/devdesk-lib.sh"

usage() {
    sed -n '2,10s/^# \{0,1\}//p' "$0"
}

# Tab-separated running sessions: name, dir, attached clients, last activity, windows.
running_tsv() {
    dd_server_up || return 0
    dd_tmux list-sessions -F '#{session_name}	#{session_path}	#{session_attached}	#{session_activity}	#{session_windows}' 2>/dev/null || true
}

# Tab-separated top-level project directories: slug, dir.
projects_tsv() {
    local d real root
    root=$(realpath -e -- "$DEVDESK_PROJECTS_ROOT" 2>/dev/null) || return 0
    for d in "$DEVDESK_PROJECTS_ROOT"/*/; do
        [ -d "$d" ] || continue
        d=${d%/}
        case ${d##*/} in node_modules | _* | lost+found) continue ;; esac
        # Only offer directories that attach would accept (no symlinks out of the root).
        real=$(realpath -e -- "$d" 2>/dev/null) || continue
        case "$real/" in "$root"/*) ;; *) continue ;; esac
        printf '%s\t%s\n' "$(dd_slugify "${d##*/}")" "$d"
    done
}

# {"<session>": {model, used_pct, ...}} from the status-line snapshots.
context_json() {
    local f
    if [ -d "$DEVDESK_CONTEXT_DIR" ]; then
        for f in "$DEVDESK_CONTEXT_DIR"/*.json; do
            [ -f "$f" ] || continue
            jq -cM --arg n "$(basename "$f" .json)" '{($n): .}' "$f" 2>/dev/null || true
        done
    fi | jq -cMs 'add // {}'
}

cmd_list() {
    local reg='{"sessions":{}}'
    [ -s "$DEVDESK_REGISTRY" ] && reg=$(cat "$DEVDESK_REGISTRY")
    jq -cMn \
        --argjson reg "$reg" \
        --arg run "$(running_tsv)" \
        --arg proj "$(projects_tsv)" \
        --argjson ctx "$(context_json)" \
        --arg dflt "$DEVDESK_DEFAULT_SESSION" \
        --arg root "$DEVDESK_PROJECTS_ROOT" '
        def rows($s): $s | split("\n") | map(select(length > 0) | split("\t"));
        ($reg.sessions // {}) as $known
        | [rows($run)[] | {name: .[0], dir: .[1], running: true,
                           attached: (.[2] | tonumber), activity: (.[3] | tonumber),
                           windows: (.[4] | tonumber)}] as $live
        | ($live | map(.name)) as $liveNames
        | {default: $dflt, root: $root,
           sessions: (
             ($live | map(. + {claude_session_id: ($known[.name].claude_session_id // null)}))
             + ($known | to_entries
                 | map(select(.key as $k | $liveNames | index($k) | not))
                 | map({name: .key, dir: .value.dir, running: false, attached: 0,
                        activity: null, windows: 0,
                        claude_session_id: (.value.claude_session_id // null)}))
           ) | map(. + {context: ($ctx[.name] // null)}),
           projects: ([{name: $dflt, dir: $root}]
                      + [rows($proj)[] | {name: .[0], dir: .[1]}
                         | select(.name | test("^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$"))])}'
}

cmd_ls() {
    cmd_list | jq -Mr '
        .sessions | if length == 0 then "no sessions" else
        (["", "SESSION", "CLIENTS", "CONTEXT", "DIRECTORY"] | @tsv),
        (.[] | [(if .running then "●" else "○" end), .name, (.attached | tostring),
                (if .context.used_pct then "\(.context.used_pct | floor)%" else "-" end), .dir] | @tsv)
        end' | column -t -s $'\t'
}

session_exists() {
    { dd_server_up && dd_tmux has-session -t "=$1" 2>/dev/null; } || [ -n "$(dd_registry_get "$1" dir)" ]
}

create_detached() {
    local name=$1 dir=$2 args
    dd_tmux has-session -t "=$name" 2>/dev/null && return 0
    mapfile -d '' args < <(dd_new_session_args "$name" "$dir")
    # Another client may win the race; "duplicate session" is fine.
    dd_tmux new-session -d "${args[@]}" 2>/dev/null || dd_tmux has-session -t "=$name" 2>/dev/null ||
        dd_die "could not create session '$name'"
    dd_registry_set_dir "$name" "$dir"
}

cmd_new() {
    { [ $# -ge 1 ] && [ $# -le 2 ]; } || dd_die "usage: devdesk new NAME [PROJECT]"
    dd_valid_name "$1" || dd_die "invalid session name: $1"
    local dir
    if [ $# -eq 2 ]; then
        dd_valid_name "$2" || dd_die "invalid project name: $2"
        dir=$(dd_project_dir "$2") || dd_die "no project '$2' under $DEVDESK_PROJECTS_ROOT"
        ! session_exists "$1" || dd_die "a session named '$1' already exists"
    else
        dir=$(dd_resolve_dir "$1")
    fi
    dd_ensure_server
    create_detached "$1" "$dir"
}

cmd_rename() {
    [ $# -eq 2 ] || dd_die "usage: devdesk rename OLD NEW"
    local old=$1 new=$2 running=0
    dd_valid_name "$old" || dd_die "invalid session name: $old"
    dd_valid_name "$new" || dd_die "invalid session name: $new"
    [ "$old" != "$new" ] || return 0
    if dd_server_up && dd_tmux has-session -t "=$old" 2>/dev/null; then running=1; fi
    [ "$running" = 1 ] || [ -n "$(dd_registry_get "$old" dir)" ] || dd_die "no session named '$old'"
    ! session_exists "$new" || dd_die "a session named '$new' already exists"
    if [ "$running" = 1 ]; then
        dd_tmux rename-session -t "=$old" "$new"
        dd_tmux set-environment -t "=$new" DEVDESK_SESSION "$new"
    fi
    dd_registry_rename "$old" "$new"
}

cmd_end() {
    [ $# -eq 1 ] || dd_die "usage: devdesk end NAME"
    dd_valid_name "$1" || dd_die "invalid session name: $1"
    if dd_server_up; then
        dd_tmux kill-session -t "=$1" 2>/dev/null || true
    fi
    dd_registry_remove "$1"
}

ok_json() {
    printf '{"ok":true}\n'
}

cmd_attach() {
    if [ $# -eq 0 ]; then
        exec "$DEVDESK_LIBEXEC/devdesk-picker"
    fi
    case $1 in
    --list)
        [ $# -eq 1 ] || dd_die "--list takes no arguments"
        cmd_list
        return
        ;;
    --new | --rename | --end)
        local action=${1#--}
        shift
        "cmd_$action" "$@"
        ok_json
        return
        ;;
    -*) dd_die "unknown option: $1" ;;
    esac
    [ $# -eq 1 ] || dd_die "attach takes exactly one session name"
    local name=$1 dir args
    dd_valid_name "$name" || dd_die "invalid session name: $name"
    dd_ensure_server
    dir=$(dd_resolve_dir "$name")

    # Already inside a DevDesk pane: switch this client instead of nesting.
    local outer=${TMUX:-}
    if [ -n "$outer" ] && [ "${outer%%,*}" = "$DEVDESK_SOCKET" ]; then
        create_detached "$name" "$dir"
        exec tmux -N -S "$DEVDESK_SOCKET" switch-client -t "=$name"
    fi
    unset TMUX

    if ! dd_tmux has-session -t "=$name" 2>/dev/null; then
        dd_registry_set_dir "$name" "$dir"
    fi
    mapfile -d '' args < <(dd_new_session_args "$name" "$dir")
    # -A attaches if the session exists, so two tabs racing never create a duplicate.
    exec tmux -N -u -S "$DEVDESK_SOCKET" new-session -A "${args[@]}"
}

cmd=${1:-help}
[ $# -gt 0 ] && shift
case $cmd in
attach | a) cmd_attach "$@" ;;
new) cmd_new "$@" ;;
rename | mv) cmd_rename "$@" ;;
ls | list) cmd_ls ;;
--list) cmd_list ;;
end) cmd_end "$@" ;;
pick) exec "$DEVDESK_LIBEXEC/devdesk-picker" ;;
help | -h | --help) usage ;;
*)
    usage >&2
    exit 2
    ;;
esac
