#!/usr/bin/env bash
# cc-session: Analyze Claude Code session JSONL logs
# Zero dependencies beyond jq (required) and standard coreutils.
#
# Usage:
#   cc-session <command> [session.jsonl] [options]
#   cc-session <command> --project <project-name> [options]
#   cc-session <command> --dir <directory-pattern> [options]
#
# If no file given, uses the most recent session in the current project.

set -euo pipefail

CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}"
PROJECTS_DIR="$CLAUDE_DIR/projects"

# --- Formatting ---

# Colors (disabled if not a terminal or NO_COLOR is set)
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
  C_RESET='\033[0m'
  C_BOLD='\033[1m'
  C_DIM='\033[2m'
  C_CYAN='\033[36m'
  C_GREEN='\033[32m'
  C_YELLOW='\033[33m'
  C_BLUE='\033[34m'
  C_MAGENTA='\033[35m'
  C_RED='\033[31m'
  C_WHITE='\033[37m'
else
  C_RESET='' C_BOLD='' C_DIM='' C_CYAN='' C_GREEN=''
  C_YELLOW='' C_BLUE='' C_MAGENTA='' C_RED='' C_WHITE=''
fi

header()  { printf "${C_BOLD}${C_CYAN}%s${C_RESET}\n" "$1"; }
subhead() { printf "\n${C_BOLD}%s${C_RESET}\n" "$1"; }
label()   { printf "  ${C_DIM}%-16s${C_RESET} %s\n" "$1" "$2"; }
divider() { printf "${C_DIM}%s${C_RESET}\n" "$(printf '%.0s-' {1..50})"; }
bar() {
  # Usage: bar <value> <max> <width>
  local val=${1:-0} max=${2:-1} width=${3:-20}
  [[ $max -le 0 ]] && max=1
  local filled=$(( val * width / max ))
  [[ $filled -gt $width ]] && filled=$width
  local empty=$(( width - filled ))
  local filled_str="" empty_str=""
  local i
  for (( i=0; i<filled; i++ )); do filled_str+="█"; done
  for (( i=0; i<empty; i++ )); do empty_str+="░"; done
  printf "${C_GREEN}%s${C_DIM}%s${C_RESET}" "$filled_str" "$empty_str"
}

# --- Helpers ---

die() { printf "${C_RED}error:${C_RESET} %s\n" "$1" >&2; exit 1; }

usage() {
  cat <<'USAGE'
cc-session - Claude Code session log analyzer

COMMANDS:
  sessions           List recent sessions with size and age
  overview           Entry type counts, duration, model info
  tools              Tool usage frequency (sorted)
  tool-chain         Sequential tool call trace with timing
  thinking           Extract thinking/reasoning blocks
  thinking-summary   First 200 chars of each thinking block
  errors             Tool results containing errors
  conversation       Reconstructed user/assistant turns
  files              Files read, edited, or written
  turns              Per-turn breakdown (duration, tools, tokens)
  agents             Subagent spawns and their tool usage
  search <pattern>   Search across sessions (user + assistant text)
  cost               Rough token/cost estimation
  timeline           Event timeline with gaps highlighted
  summary            Session summaries (compaction boundaries)

OPTIONS:
  --project, -p <name>    Filter by project name (partial match)
  --dir, -d <pattern>     Filter by directory pattern in project path
  --all                   Search all projects (with search command)
  --recent <n>            Use nth most recent session (default: 1)
  --json                  Output as JSON instead of text

EXAMPLES:
  cc-session overview                           # Current project, latest session
  cc-session tools                              # Tool frequency
  cc-session tools --recent 2                   # Second most recent session
  cc-session search "auth" --all                # Search all projects
  cc-session errors -p claude-mods              # Errors in claude-mods project
  cc-session tool-chain                         # Full tool call sequence
  cc-session thinking | grep -i "decision"      # Search reasoning
  cc-session turns --json | jq '.[] | select(.tools > 5)'
USAGE
  exit 0
}

# Resolve project directory from current working directory
resolve_project() {
  local project_filter="${1:-}"
  local candidates=()

  if [[ -n "$project_filter" ]]; then
    # Find by partial match - collect all matches
    while IFS= read -r d; do
      candidates+=("$d")
    done < <(ls "$PROJECTS_DIR" 2>/dev/null | grep -i "$project_filter")
  else
    # Derive from cwd
    local encoded
    encoded=$(pwd | sed 's/[:\\\/]/-/g' | sed 's/--*/-/g')
    while IFS= read -r d; do
      candidates+=("$d")
    done < <(ls "$PROJECTS_DIR" 2>/dev/null | grep -i "${encoded##*-}")
  fi

  [[ ${#candidates[@]} -gt 0 ]] || die "No project matching '${project_filter:-$(pwd)}'"

  # Pick the candidate that actually has JSONL files, preferring most recent
  for candidate in "${candidates[@]}"; do
    local dir="$PROJECTS_DIR/$candidate"
    # Check if directory contains any .jsonl files (not just subdirs)
    if ls "$dir"/*.jsonl &>/dev/null; then
      echo "$dir"
      return
    fi
  done

  # Fallback: return first match even without JSONL files
  echo "$PROJECTS_DIR/${candidates[0]}"
}

# Resolve session file
resolve_session() {
  local project_dir="$1"
  local recent="${2:-1}"
  # Use ls on directory then filter - portable across Git Bash / macOS / Linux
  ls -t "$project_dir/" 2>/dev/null | grep '\.jsonl$' | grep -v '^agent-' | sed -n "${recent}p" |
    while read -r f; do echo "$project_dir/$f"; done
}

# --- Commands ---

cmd_sessions() {
  local project_dir="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    ls -t "$project_dir/" 2>/dev/null | grep '\.jsonl$' | grep -v '^agent-' | while read -r fname; do
      local fpath="$project_dir/$fname"
      local session_id="${fname%.jsonl}"
      local size
      size=$(wc -c < "$fpath" 2>/dev/null | tr -d ' ')
      local mod_date
      mod_date=$(date -r "$fpath" '+%Y-%m-%d %H:%M' 2>/dev/null || stat -c '%y' "$fpath" 2>/dev/null | cut -d. -f1)
      printf '{"session":"%s","size_bytes":%s,"modified":"%s"}\n' "$session_id" "$size" "$mod_date"
    done
  else
    header "Sessions"
    echo ""
    printf "  ${C_DIM}%-4s %-38s %6s  %s${C_RESET}\n" "#" "Session ID" "Size" "Last Modified"
    divider
    local n=0
    ls -t "$project_dir/" 2>/dev/null | grep '\.jsonl$' | grep -v '^agent-' | while read -r fname; do
      n=$((n + 1))
      local fpath="$project_dir/$fname"
      local session_id="${fname%.jsonl}"
      # Human-readable size
      local bytes
      bytes=$(wc -c < "$fpath" 2>/dev/null | tr -d ' ')
      local size_h
      if [[ $bytes -ge 1048576 ]]; then
        size_h="$(( bytes / 1048576 ))M"
      elif [[ $bytes -ge 1024 ]]; then
        size_h="$(( bytes / 1024 ))K"
      else
        size_h="${bytes}B"
      fi
      local mod_date
      mod_date=$(date -r "$fpath" '+%Y-%m-%d %H:%M' 2>/dev/null || stat -c '%y' "$fpath" 2>/dev/null | cut -d. -f1)
      if [[ $n -eq 1 ]]; then
        printf "  ${C_GREEN}%-4s${C_RESET} ${C_BOLD}%-38s${C_RESET} %6s  %s  ${C_GREEN}(latest)${C_RESET}\n" "$n" "$session_id" "$size_h" "$mod_date"
      else
        printf "  ${C_DIM}%-4s${C_RESET} %-38s %6s  %s\n" "$n" "$session_id" "$size_h" "$mod_date"
      fi
    done
    echo ""
    printf "  ${C_DIM}Use --recent <n> to select a session, e.g.: cc-session overview --recent 3${C_RESET}\n"
    printf "  ${C_DIM}Resume with: claude --resume <full-session-id>${C_RESET}\n"
  fi
}

cmd_overview() {
  local f="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    cat "$f" | jq -sc '{
      file: input_filename,
      entries: length,
      types: (group_by(.type) | map({type: .[0].type, count: length})),
      first_ts: (map(.timestamp | select(.) | strings) | sort | first),
      last_ts: (map(.timestamp | select(.) | strings) | sort | last),
      duration_ms: (
        [.[] | select(.type == "system" and .subtype == "turn_duration") | .durationMs] | add
      ),
      turns: ([.[] | select(.type == "system" and .subtype == "turn_duration")] | length),
      tool_calls: ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use")] | length),
      thinking_blocks: ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "thinking")] | length),
      user_messages: ([.[] | select(.type == "user") | .message.content[]? | select(.type == "text")] | length)
    }' 2>/dev/null
  else
    # Gather all stats in one jq pass
    local stats
    stats=$(cat "$f" | jq -rsc '
      ([.[] | select(.type == "system" and .subtype == "turn_duration") | .durationMs] | add // 0) as $total_ms |
      ([.[] | select(.type == "system" and .subtype == "turn_duration")] | length) as $turns |
      ([.[] | select(.type == "user") | .message.content[]? | select(.type == "text")] | length) as $user_msgs |
      ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use")] | length) as $tools |
      ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "thinking")] | length) as $thinking |
      ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | .name] |
        group_by(.) | map({t: .[0], n: length}) | sort_by(-.n) | .[:5]) as $top_tools |
      (map(.timestamp | select(.) | strings) | sort) as $ts |
      ([.[] | select(.type == "user") | .message.content[]? | select(.type == "text") | .text | length] | add // 0) as $user_chars |
      ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text | length] | add // 0) as $asst_chars |
      ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "thinking") | .thinking | length] | add // 0) as $think_chars |
      (($user_chars + $asst_chars + $think_chars) / 4 | floor) as $est_tokens |
      {
        total_ms: $total_ms,
        total_s: ($total_ms / 1000 | floor),
        mins: ($total_ms / 60000 | floor),
        secs: (($total_ms / 1000 | floor) % 60),
        turns: $turns,
        avg_s: (if $turns > 0 then ($total_ms / $turns / 1000 | floor) else 0 end),
        user_msgs: $user_msgs,
        tools: $tools,
        thinking: $thinking,
        top_tools: $top_tools,
        entries: length,
        first_ts: ($ts | first // "unknown"),
        last_ts: ($ts | last // "unknown"),
        est_tokens_k: (($est_tokens / 1000 * 10 | floor) / 10)
      } | @json
    ' 2>/dev/null)

    local session_id
    session_id=$(basename "$f" .jsonl)

    header "Session Overview"
    echo ""
    label "Session" "$session_id"
    label "Resume" "claude --resume $session_id"
    label "Started" "$(echo "$stats" | jq -r '.first_ts | .[0:19] | gsub("T"; " ")')"
    label "Ended" "$(echo "$stats" | jq -r '.last_ts | .[0:19] | gsub("T"; " ")')"
    label "Entries" "$(echo "$stats" | jq -r '.entries')"

    subhead "Timing"
    divider
    local mins secs turns avg
    mins=$(echo "$stats" | jq -r '.mins')
    secs=$(echo "$stats" | jq -r '.secs')
    turns=$(echo "$stats" | jq -r '.turns')
    avg=$(echo "$stats" | jq -r '.avg_s')
    label "Duration" "${mins}m ${secs}s"
    label "Turns" "$turns"
    label "Avg turn" "${avg}s"

    subhead "Activity"
    divider
    local user_msgs tools thinking
    user_msgs=$(echo "$stats" | jq -r '.user_msgs')
    tools=$(echo "$stats" | jq -r '.tools')
    thinking=$(echo "$stats" | jq -r '.thinking')
    local est_tokens
    est_tokens=$(echo "$stats" | jq -r '.est_tokens_k')
    label "User messages" "$user_msgs"
    label "Tool calls" "$tools"
    label "Thinking" "$thinking blocks"
    label "Est. tokens" "${est_tokens}k"

    subhead "Top Tools"
    divider
    echo "$stats" | jq -r '.top_tools[] | "\(.t)\t\(.n)"' | tr -d '\r' | while IFS=$'\t' read -r tool count; do
      printf "  ${C_YELLOW}%-16s${C_RESET} %3s  " "$tool" "$count"
      bar "$count" "$tools" 20
      echo ""
    done
  fi
}

cmd_tools() {
  local f="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    cat "$f" | jq -sc '[
      .[] | select(.type == "assistant") | .message.content[]? |
      select(.type == "tool_use") | .name
    ] | group_by(.) | map({tool: .[0], count: length}) | sort_by(-.count)' 2>/dev/null
  else
    header "Tool Usage"
    echo ""
    local total
    total=$(cat "$f" | jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | .name' | wc -l)
    cat "$f" | jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | .name' |
      sort | uniq -c | sort -rn | tr -d '\r' | while read -r count tool; do
        printf "  ${C_YELLOW}%-16s${C_RESET} %3s  " "$tool" "$count"
        bar "$count" "$total" 25
        echo ""
      done
    divider
    label "Total" "$total calls"
  fi
}

cmd_tool_chain() {
  local f="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    cat "$f" | jq -c '
      select(.type == "assistant") | .message.content[]? |
      select(.type == "tool_use") |
      {name, id, input_summary: (
        if .name == "Bash" then (.input.command | .[0:120])
        elif .name == "Read" then .input.file_path
        elif .name == "Write" then .input.file_path
        elif .name == "Edit" then .input.file_path
        elif .name == "Grep" then "\(.input.pattern) in \(.input.path // ".")"
        elif .name == "Glob" then .input.pattern
        elif .name == "Agent" then "\(.input.subagent_type // "general"): \(.input.description // "")"
        elif .name == "Skill" then .input.skill
        elif .name == "WebSearch" then .input.query
        elif .name == "WebFetch" then .input.url
        else (.input | tostring | .[0:100])
        end
      )}
    ' 2>/dev/null
  else
    cat "$f" | jq -r '
      select(.type == "assistant") | .message.content[]? |
      select(.type == "tool_use") |
      "\(.name | . + " " * (15 - length))  \(
        if .name == "Bash" then (.input.command | .[0:100])
        elif .name == "Read" then .input.file_path
        elif .name == "Write" then .input.file_path
        elif .name == "Edit" then .input.file_path
        elif .name == "Grep" then "pattern=\(.input.pattern) path=\(.input.path // ".")"
        elif .name == "Glob" then .input.pattern
        elif .name == "Agent" then "\(.input.subagent_type // "general"): \(.input.description // "")"
        elif .name == "Skill" then .input.skill
        elif .name == "WebSearch" then .input.query
        elif .name == "WebFetch" then .input.url
        else (.input | tostring | .[0:80])
        end
      )"
    ' 2>/dev/null
  fi
}

cmd_thinking() {
  local f="$1"
  cat "$f" | jq -r '
    select(.type == "assistant") | .message.content[]? |
    select(.type == "thinking") | .thinking
  ' 2>/dev/null
}

cmd_thinking_summary() {
  local f="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    cat "$f" | jq -sc '[
      .[] | select(.type == "assistant") | .message.content[]? |
      select(.type == "thinking") | {preview: (.thinking | .[0:200])}
    ]' 2>/dev/null
  else
    local n=0
    cat "$f" | jq -r '
      select(.type == "assistant") | .message.content[]? |
      select(.type == "thinking") | .thinking | .[0:200] | gsub("\n"; " ")
    ' 2>/dev/null | while read -r line; do
      n=$((n + 1))
      printf "[%d] %s...\n\n" "$n" "$line"
    done
  fi
}

cmd_errors() {
  local f="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    cat "$f" | jq -c '
      select(.type == "user") | .message.content[]? |
      select(.type == "tool_result") |
      select(.content | type == "string" and test("error|Error|ERROR|failed|Failed|FAILED")) |
      {tool_use_id, error: (.content | .[0:300])}
    ' 2>/dev/null
  else
    cat "$f" | jq -r '
      select(.type == "user") | .message.content[]? |
      select(.type == "tool_result") |
      select(.content | type == "string" and test("error|Error|ERROR|failed|Failed|FAILED")) |
      "--- tool_use_id: \(.tool_use_id) ---\n\(.content | .[0:300])\n"
    ' 2>/dev/null
  fi
}

cmd_conversation() {
  local f="$1"
  cat "$f" | jq -r '
    if .type == "user" then
      .message.content[]? | select(.type == "text") | "USER: \(.text)"
    elif .type == "assistant" then
      .message.content[]? | select(.type == "text") | "CLAUDE: \(.text | .[0:500])"
    else empty end
  ' 2>/dev/null
}

cmd_files() {
  local f="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    cat "$f" | jq -sc '{
      read: [.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "Read") | .input.file_path] | group_by(.) | map({file: .[0], count: length}) | sort_by(-.count),
      edited: [.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "Edit") | .input.file_path] | group_by(.) | map({file: .[0], count: length}) | sort_by(-.count),
      written: [.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "Write") | .input.file_path] | unique
    }' 2>/dev/null
  else
    header "Files Touched"
    subhead "Read"
    divider
    cat "$f" | jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "Read") | .input.file_path' |
      sort | uniq -c | sort -rn | while read -r count path; do
        local short="${path##*/}"
        printf "  ${C_DIM}%3s${C_RESET}  ${C_BLUE}%s${C_RESET}\n" "$count" "$short"
      done
    subhead "Edited"
    divider
    cat "$f" | jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "Edit") | .input.file_path' |
      sort | uniq -c | sort -rn | while read -r count path; do
        local short="${path##*/}"
        printf "  ${C_DIM}%3s${C_RESET}  ${C_YELLOW}%s${C_RESET}\n" "$count" "$short"
      done
    subhead "Written (new files)"
    divider
    cat "$f" | jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "Write") | .input.file_path' |
      sort -u | while read -r path; do
        local short="${path##*/}"
        printf "  ${C_GREEN}+ %s${C_RESET}\n" "$short"
      done
  fi
}

cmd_turns() {
  local f="$1" json="${2:-}"
  cat "$f" | jq -sc '
    [.[] | select(.type == "system" and .subtype == "turn_duration")] as $durations |
    [.[] | select(.type == "assistant")] as $assistants |
    [range(0; [$durations | length, $assistants | length] | min) |
      {
        turn: (. + 1),
        duration_s: ($durations[.].durationMs / 1000 | floor),
        tools: ([$assistants[.].message.content[]? | select(.type == "tool_use") | .name] | length),
        tool_names: ([$assistants[.].message.content[]? | select(.type == "tool_use") | .name]),
        has_thinking: ([$assistants[.].message.content[]? | select(.type == "thinking")] | length > 0),
        text_length: ([$assistants[.].message.content[]? | select(.type == "text") | .text | length] | add // 0)
      }
    ]
  ' 2>/dev/null | if [[ "$json" == "json" ]]; then cat; else
    local max_dur
    max_dur=$(jq '[.[].duration_s] | max // 1' <<< "$(cat)")
    # Re-read from the pipe above (need to re-run since we consumed stdin)
    cat "$f" | jq -sc '
      [.[] | select(.type == "system" and .subtype == "turn_duration")] as $durations |
      [.[] | select(.type == "assistant")] as $assistants |
      ([.[] | select(.type == "system" and .subtype == "turn_duration") | .durationMs] | max // 1000) as $max_ms |
      [range(0; [$durations | length, $assistants | length] | min) |
        {
          turn: (. + 1),
          duration_s: ($durations[.].durationMs / 1000 | floor),
          max_s: ($max_ms / 1000 | floor),
          tools: ([$assistants[.].message.content[]? | select(.type == "tool_use") | .name] | length),
          tool_names: ([$assistants[.].message.content[]? | select(.type == "tool_use") | .name]),
          has_thinking: ([$assistants[.].message.content[]? | select(.type == "thinking")] | length > 0)
        }
      ]
    ' 2>/dev/null | jq -r '.[] | "\(.turn)\t\(.duration_s)\t\(.max_s)\t\(.tools)\t\(.tool_names | join(", ") | if . == "" then "-" else . end)\t\(.has_thinking)"' | tr -d '\r' |
    {
      header "Turns"
      echo ""
      printf "  ${C_DIM}%-6s %-8s %-6s %-8s %s${C_RESET}\n" "Turn" "Time" "Tools" "" "Details"
      divider
      while IFS=$'\t' read -r turn dur max_s tools tool_names thinking; do
        thinking=$(echo "$thinking" | tr -d '\r ')
        local think_flag=""
        [[ "$thinking" == "true" ]] && think_flag=" ${C_MAGENTA}[T]${C_RESET}"
        printf "  ${C_BOLD}%-6s${C_RESET} %5ss  " "#$turn" "$dur"
        bar "$dur" "$max_s" 12
        printf " %2s  ${C_DIM}%s${C_RESET}%b\n" "$tools" "$tool_names" "$think_flag"
      done
    }
  fi
}

cmd_agents() {
  local f="$1" json="${2:-}"
  if [[ "$json" == "json" ]]; then
    cat "$f" | jq -c '
      select(.type == "assistant") | .message.content[]? |
      select(.type == "tool_use" and .name == "Agent") |
      {
        id,
        subagent_type: (.input.subagent_type // "general-purpose"),
        description: .input.description,
        prompt_preview: (.input.prompt | .[0:200]),
        background: (.input.run_in_background // false),
        isolation: (.input.isolation // null)
      }
    ' 2>/dev/null
  else
    cat "$f" | jq -r '
      select(.type == "assistant") | .message.content[]? |
      select(.type == "tool_use" and .name == "Agent") |
      "[\(.input.subagent_type // "general")] \(.input.description // "no description")\n  prompt: \(.input.prompt | .[0:150] | gsub("\n"; " "))...\n"
    ' 2>/dev/null
  fi
}

cmd_search() {
  local pattern="$1"
  shift
  local search_dir="$1"

  rg -l "$pattern" "$search_dir"/*.jsonl 2>/dev/null | while read -r f; do
    local session
    session=$(basename "$f" .jsonl)
    echo "=== $session ==="
    cat "$f" | jq -r "
      if .type == \"user\" then
        .message.content[]? | select(.type == \"text\") | .text
      elif .type == \"assistant\" then
        .message.content[]? | select(.type == \"text\") | .text
      else empty end
    " 2>/dev/null | grep -i --color=auto -C2 "$pattern" || true
    echo ""
  done
}

cmd_cost() {
  local f="$1" json="${2:-}"
  cat "$f" | jq -sc '
    ([.[] | select(.type == "user") | .message.content[]? | select(.type == "text") | .text | length] | add // 0) as $user_chars |
    ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text | length] | add // 0) as $asst_chars |
    ([.[] | select(.type == "assistant") | .message.content[]? | select(.type == "thinking") | .thinking | length] | add // 0) as $think_chars |
    ($user_chars + $asst_chars + $think_chars) as $total_chars |
    ($total_chars / 4 | floor) as $est_tokens |
    {
      user_chars: $user_chars,
      assistant_chars: $asst_chars,
      thinking_chars: $think_chars,
      total_chars: $total_chars,
      est_tokens: $est_tokens,
      est_tokens_k: (($est_tokens / 1000 * 10 | floor) / 10)
    }
  ' 2>/dev/null | if [[ "$json" == "json" ]]; then cat; else
    jq -r '.' | {
      header "Cost Estimate"
      echo ""
      local data
      data=$(cat)
      local est_k user asst think total
      est_k=$(echo "$data" | jq -r '.est_tokens_k')
      user=$(echo "$data" | jq -r '.user_chars')
      asst=$(echo "$data" | jq -r '.assistant_chars')
      think=$(echo "$data" | jq -r '.thinking_chars')
      total=$(echo "$data" | jq -r '.total_chars')
      label "Est. tokens" "${C_BOLD}${est_k}k${C_RESET}"
      label "User" "${user} chars"
      label "Assistant" "${asst} chars"
      label "Thinking" "${think} chars"
      divider
      label "Total chars" "$total"
    }
  fi
}

cmd_timeline() {
  local f="$1"
  cat "$f" | jq -r '
    select(.type == "user" or .type == "assistant") |
    select(.timestamp) |
    "\(.timestamp)  \(.type | . + " " * (12 - length))  \(
      if .type == "user" then
        (.message.content[]? | select(.type == "text") | .text | .[0:80] | gsub("\n"; " ")) // "[tool_result]"
      else
        (.message.content[]? |
          if .type == "tool_use" then "[\(.name)] \(
            if .name == "Bash" then (.input.command | .[0:60])
            elif .name == "Read" then .input.file_path
            elif .name == "Edit" then .input.file_path
            elif .name == "Write" then .input.file_path
            else (.input | tostring | .[0:60])
            end
          )"
          elif .type == "text" then .text | .[0:80] | gsub("\n"; " ")
          elif .type == "thinking" then "[thinking...]"
          else empty
          end
        ) // ""
      end
    )"
  ' 2>/dev/null
}

cmd_summary() {
  local f="$1"
  cat "$f" | jq -r '
    select(.type == "summary" or (.type == "system" and .subtype == "compact_boundary")) |
    if .type == "summary" then
      "=== Summary ===\n\(.summary)\n"
    else
      "--- Compaction Boundary ---"
    end
  ' 2>/dev/null
}

# --- Main ---

[[ $# -eq 0 ]] && usage

cmd="$1"
shift

# Parse options
session_file=""
project=""
recent=1
output="text"
search_all=false
dir_pattern=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --project|-p) project="$2"; shift 2 ;;
    --dir|-d) dir_pattern="$2"; shift 2 ;;
    --recent) recent="$2"; shift 2 ;;
    --json) output="json"; shift ;;
    --all) search_all=true; shift ;;
    --help|-h) usage ;;
    *)
      if [[ -f "$1" ]]; then
        session_file="$1"
      elif [[ "$cmd" == "search" && -z "${search_pattern:-}" ]]; then
        search_pattern="$1"
      fi
      shift
      ;;
  esac
done

# Resolve session file if not given directly
if [[ -z "$session_file" ]]; then
  project_dir=$(resolve_project "${project}${dir_pattern:+$dir_pattern}")
  session_file=$(resolve_session "$project_dir" "$recent")
  [[ -n "$session_file" ]] || die "No session files found in $project_dir"
fi

# Execute command
case "$cmd" in
  sessions)
    project_dir=$(resolve_project "${project}${dir_pattern:+$dir_pattern}")
    cmd_sessions "$project_dir" "$output"
    exit 0
    ;;
  overview)          cmd_overview "$session_file" "$output" ;;
  tools)             cmd_tools "$session_file" "$output" ;;
  tool-chain)        cmd_tool_chain "$session_file" "$output" ;;
  thinking)          cmd_thinking "$session_file" ;;
  thinking-summary)  cmd_thinking_summary "$session_file" "$output" ;;
  errors)            cmd_errors "$session_file" "$output" ;;
  conversation)      cmd_conversation "$session_file" ;;
  files)             cmd_files "$session_file" "$output" ;;
  turns)             cmd_turns "$session_file" "$output" ;;
  agents)            cmd_agents "$session_file" "$output" ;;
  cost)              cmd_cost "$session_file" "$output" ;;
  timeline)          cmd_timeline "$session_file" ;;
  summary)           cmd_summary "$session_file" ;;
  search)
    [[ -n "${search_pattern:-}" ]] || die "search requires a pattern"
    if [[ "$search_all" == true ]]; then
      for d in "$PROJECTS_DIR"/*/; do
        echo ">>> $(basename "$d")"
        cmd_search "$search_pattern" "$d"
      done
    else
      project_dir=$(resolve_project "${project}${dir_pattern:+$dir_pattern}")
      cmd_search "$search_pattern" "$project_dir"
    fi
    ;;
  *)
    die "Unknown command: $cmd. Run cc-session --help for usage."
    ;;
esac
