#!/usr/bin/env bash
# fzf-ai: fuzzy browser for AI coding sessions across claude, codex,
# opencode, droid, and pi. Selecting an item resumes it in its native CLI.
#
# Keybinds (shown in fzf header):
#   enter    resume the session in its tool
#   ctrl-o   cd into the session's working directory in a new shell
#   ctrl-e   open the raw session file in $EDITOR
#   ctrl-y   copy session id to the clipboard (when supported)
#   ctrl-k   copy the exact resume command to the clipboard
#   ctrl-p   move up
#   ctrl-n   move down
#   ctrl-r   reload the index
#   ctrl-s   cycle search scope
#   ?        toggle preview
#   alt-p    cycle preview position
#   alt-1..5 filter to one agent only
#   alt-0    clear filter
set -uo pipefail

SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
INDEX="$SCRIPT_DIR/fzf-ai-index"
PREVIEW="$SCRIPT_DIR/fzf-ai-preview"
RESUME="$SCRIPT_DIR/fzf-ai-resume"
UI="$SCRIPT_DIR/fzf-ai-ui"

version_num() {
  local major minor patch
  IFS=. read -r major minor patch <<<"${1%%[^0-9.]*}"
  major=${major:-0}
  minor=${minor:-0}
  patch=${patch:-0}
  printf '%03d%03d%03d\n' "$major" "$minor" "$patch"
}

version_ge() {
  [[ $(version_num "$1") -ge $(version_num "$2") ]]
}

if ! command -v fzf >/dev/null 2>&1; then
  echo "fzf-ai: fzf is required (https://github.com/junegunn/fzf)" >&2
  exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
  echo "fzf-ai: python3 is required" >&2
  exit 1
fi
fzf_version=$(fzf --version | awk '{print $1}')
if ! version_ge "$fzf_version" "0.63.0"; then
  echo "fzf-ai: fzf >= 0.63.0 is required (found $fzf_version)" >&2
  exit 1
fi

# --- Index column layout -------------------------------------------------
# The indexer emits 9 TAB-separated columns per line:
#   1 agent-raw   2 session_id  3 source        (hidden, machine-only)
#   4 agent-badge 5 updated     6 msgs          (visible, padded)
#   7 cwd         8 title                       (visible)
#   9 search-blob (rendered but off-screen)     -- hidden session content
# fzf's --nth indexes into the --with-nth view, so the visible columns
# are: 4,5,6,7,8,9 = positions 1,2,3,4,5,6 in the transformed view.
# Column 9 is deliberately pushed off-screen so session content is searchable
# without cluttering the list rows.
# --------------------------------------------------------------------------

copy_cmd() {
  if command -v pbcopy >/dev/null 2>&1; then echo pbcopy; return; fi
  if command -v wl-copy >/dev/null 2>&1; then echo wl-copy; return; fi
  if command -v xclip  >/dev/null 2>&1; then echo "xclip -selection clipboard"; return; fi
  echo ""
}
COPY=$(copy_cmd)

quote_cmd() {
  printf '%q ' "$@"
}

build_border_label() {
  if [[ $# -eq 0 ]]; then
    printf ' fzf-ai '
    return
  fi
  printf ' fzf-ai · %s ' "$*"
}

# Agents can be filtered with args: `fzf-ai claude codex`
AGENTS=("$@")
BORDER_LABEL=$(build_border_label "${AGENTS[@]}")
STATE_HOME=${XDG_STATE_HOME:-$HOME/.local/state}
HISTORY_DIR="$STATE_HOME/fzf-ai"
HISTORY_FILE="$HISTORY_DIR/query-history"
mkdir -p "$HISTORY_DIR"

# Export the children scripts so fzf bindings can call them.
export FZFAI_PREVIEW="$PREVIEW"
export FZFAI_RESUME="$RESUME"
export FZFAI_INDEX="$INDEX"
export FZFAI_UI="$UI"
export FZFAI_AGENTS="${AGENTS[*]:-}"
export FZFAI_COPY="$COPY"

# Preview command: index record passed via positional expansion.
#   {1}=agent {2}=sid {3}=source {7}=cwd {8}=title
PREVIEW_CMD='"$FZFAI_PREVIEW" {1} {2} {3} {q}'

# Reload command re-runs the indexer, optionally filtered to named agents.
RELOAD_CMD=$(quote_cmd "$FZFAI_INDEX" "${AGENTS[@]}")
RELOAD_CLAUDE_CMD=$(quote_cmd "$FZFAI_INDEX" claude)
RELOAD_CODEX_CMD=$(quote_cmd "$FZFAI_INDEX" codex)
RELOAD_OPENCODE_CMD=$(quote_cmd "$FZFAI_INDEX" opencode)
RELOAD_DROID_CMD=$(quote_cmd "$FZFAI_INDEX" droid)
RELOAD_PI_CMD=$(quote_cmd "$FZFAI_INDEX" pi)

HEADER=$'scope: [all] [agent] [cwd] [title] [content]  ·  click header or ctrl-s to change\nenter: resume  ·  ctrl-o: shell  ·  ctrl-e: edit  ·  ctrl-r: reload  ·  ?: preview  ·  alt-p: layout  ·  alt-1..5: agent'
if [[ -n "$COPY" ]]; then
  HEADER+=$'  ·  ctrl-y: copy id  ·  ctrl-k: copy cmd'
fi

# -------------------------------------------------------------------------
# Run fzf and capture the selected row. We deliberately do NOT use the
# `become` action here. `become` exec()s the target command from inside
# fzf, which we observed leaving the terminal in a state that breaks
# cleanly-written TUIs (codex hangs, opencode input freezes). Capturing
# the selection instead lets fzf fully tear down its altscreen and
# restore the terminal before we launch the target CLI.
# -------------------------------------------------------------------------
FZF_ARGS=(
  --ansi
  --delimiter=$'\t'
  --with-nth='4,5,6,7,8,9'
  --nth='1,4,5,6'
  --scheme=default
  --exact
  --no-sort
  --track
  --tiebreak=chunk,begin,length
  --info=inline-right
  --highlight-line
  --filepath-word
  --no-hscroll
  --history="$HISTORY_FILE"
  --height=100%
  --tmux='90%,90%'
  --layout=reverse
  --style='full:line'
  --border=rounded
  --border-label="$BORDER_LABEL"
  --border-label-pos=3
  --list-border=rounded
  --list-label=' Sessions '
  --input-border=rounded
  --input-label=' Search '
  --header-border=line
  --header-label=' Scope '
  --footer=' Indexing session stores… '
  --footer-border=line
  --footer-label=' Summary '
  --prompt='all> '
  --ghost='type to search sessions'
  --pointer='▶'
  --marker='✓'
  --color='fg:dim,nth:regular'
  --header="$HEADER"
  --header-lines=0
  --preview="$PREVIEW_CMD"
  --preview-label=' Preview '
  --preview-window='right,60%,border-left,wrap,~4,<120(down,55%,border-top,wrap,~4)'
  --expect=enter
  --bind="start:change-list-label( Loading sessions )+change-footer( Indexing session stores… )+reload-sync($RELOAD_CMD)"
  --bind="load:bg-cancel+transform-prompt[$UI prompt]+bg-transform-list-label[$UI list-label]+bg-transform-footer[$UI footer {*f}]"
  --bind="result:bg-cancel+transform-prompt[$UI prompt]+bg-transform-list-label[$UI list-label]+bg-transform-footer[$UI footer {*f}]"
  --bind="focus:bg-transform-preview-label[$UI preview-label {1} {2} {8}]"
  --bind="click-header:transform[$UI click-header-action]"
  --bind='ctrl-p:up'
  --bind='ctrl-n:down'
  --bind="ctrl-s:transform[$UI cycle-scope-action]"
  --bind="ctrl-r:change-list-label( Reindexing sessions )+change-footer( Rebuilding session index… )+track-current+reload-sync($RELOAD_CMD)"
  --bind='?:toggle-preview'
  --bind='alt-p:change-preview-window(right,60%,border-left,wrap,~4|down,55%,border-top,wrap,~4|hidden|)'
  --bind="ctrl-o:execute(cwd={7}; case \"\$cwd\" in '~') cwd=\"\$HOME\" ;; '~/'*) cwd=\"\$HOME/\${cwd#\~/}\" ;; esac; cd \"\$cwd\" 2>/dev/null && \$SHELL -i)"
  --bind="ctrl-e:execute(\${EDITOR:-vi} {3})"
  --bind="alt-1:change-border-label( fzf-ai · claude )+reload($RELOAD_CLAUDE_CMD)"
  --bind="alt-2:change-border-label( fzf-ai · codex )+reload($RELOAD_CODEX_CMD)"
  --bind="alt-3:change-border-label( fzf-ai · opencode )+reload($RELOAD_OPENCODE_CMD)"
  --bind="alt-4:change-border-label( fzf-ai · droid )+reload($RELOAD_DROID_CMD)"
  --bind="alt-5:change-border-label( fzf-ai · pi )+reload($RELOAD_PI_CMD)"
  --bind="alt-0:change-border-label($BORDER_LABEL)+reload($RELOAD_CMD)"
)

if [[ -n "$COPY" ]]; then
  FZF_ARGS+=(
    --bind="ctrl-y:execute-silent(printf %s {2} | \$FZFAI_COPY)"
    --bind="ctrl-k:execute-silent(\"\$FZFAI_RESUME\" --print-cmd {1} {2} {3} {7} | \$FZFAI_COPY)"
  )
fi

selection=$(fzf "${FZF_ARGS[@]}")
fzf_rc=$?

# Exit gracefully on ESC / ctrl-c / no match.
if [[ $fzf_rc -ne 0 && $fzf_rc -ne 1 ]]; then
  exit $fzf_rc
fi
[[ -z "$selection" ]] && exit 0

# --expect=enter prints the key name (or empty line for the default enter
# binding) on the first line and the selected row on the second.
row=$(printf '%s\n' "$selection" | sed -n '2p')
[[ -z "$row" ]] && exit 0

# Columns: 1 agent  2 sid  3 source  4 badge  5 date  6 msgs  7 cwd  8 title  9 blob
IFS=$'\t' read -r agent sid source _badge _date _msgs cwd _title _blob <<<"$row"
# The cwd column is space-padded for UI alignment; strip trailing spaces.
cwd=${cwd%"${cwd##*[![:space:]]}"}

if [[ -z "$agent" || -z "$sid" ]]; then
  echo "fzf-ai: could not parse selected row" >&2
  exit 2
fi

# With fzf cleanly torn down, the terminal is back in a normal state
# before we exec the AI CLI. This is what fixes the "input freeze" and
# "codex hangs" issues we saw with the `become` approach.
exec "$RESUME" "$agent" "$sid" "$source" "$cwd"
