#!/usr/bin/env bash
# Copyright (c) 2015-2026 Dotfiles. All rights reserved.
## Dotfiles AI Commands.
##
## Provides AI CLI status, setup, RAG query, and bridge commands.
## Wraps AI CLI tools with contextual patterns and system metadata.
## Usage: dot ai [delegate|cost|status]|ai-setup|ai-query|cl|copilot|gemini|kiro|sgpt|ollama|opencode|aider|autohand|vibe|qwen|zai

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../../../lib/dot/utils.sh
source "$SCRIPT_DIR/../../../lib/dot/utils.sh"

dot_ui_command_banner "AI and Agents" "${1:-}"

PATTERN_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/ai/patterns"
AI_CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/dotfiles/ai"
AI_STATUS_TTL="${DOTFILES_AI_STATUS_TTL:-300}"
AI_STATUS_CACHE_FILE="${AI_CACHE_DIR}/status.tsv"

# Fallback to source tree if patterns don't exist in config (common in CI)
if [[ ! -d "$PATTERN_DIR" ]]; then
  _AI_SRC="$(cd "$SCRIPT_DIR/../../.." && pwd)"
  if [[ -d "$_AI_SRC/dot_config/ai/patterns" ]]; then
    PATTERN_DIR="$_AI_SRC/dot_config/ai/patterns"
  fi
fi

_show_ai_bridge_usage() {
  echo "Usage: dot cl|codex|copilot|gemini|goose|kiro|autohand|vibe|qwen|zai --pattern [name] \"prompt\""
  echo ""
  echo "Available Patterns:"
  # shellcheck disable=SC2012
  ls -1 "$PATTERN_DIR" 2>/dev/null | sed 's/\.md$//' | sed 's/^/  - /' || echo "  (none)"
}

_ai_cache_fresh() {
  local file="$1"
  [[ -f "$file" ]] || return 1
  local now mtime
  now=$(date +%s)
  mtime=$(stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null || echo 0)
  ((now - mtime < AI_STATUS_TTL))
}

_ai_extract_version() {
  local bin="$1"
  local output version
  output=$("$bin" --version 2>/dev/null | head -1) || true
  version=$(printf '%s' "$output" | sed 's/^[^0-9]*//' | sed 's/[[:space:]]*$//' | sed 's/\.$//')
  [[ -n "$version" ]] && printf '%s\n' "$version" || printf 'installed\n'
}

_ai_refresh_status_cache() {
  local -n ai_entries=$1
  local tmp_file
  tmp_file="$(mktemp)"
  mkdir -p "$AI_CACHE_DIR"

  local total=${#ai_entries[@]}

  # Cold-cache refresh runs `$bin --version` for every tool — node-
  # based ones are slow to start, so the total can hit 15-30s. Show a
  # spinner so the user has feedback.
  ui_spinner_start "Probing $total AI tools (cached for ${AI_STATUS_TTL}s)"

  local jobs="${DOTFILES_AI_PROBE_JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}"
  local probe_dir
  probe_dir="$(mktemp -d)"

  # Probe via xargs -P. The probe logic lives INLINE in the bash -c
  # string rather than as an exported function, because `export -f`
  # doesn't always survive across bash invocations on every platform
  # (notably macOS bash 3.2 needs the BASH_FUNC_*() env-var format,
  # which subtly breaks for some shells in PATH). Inlining sidesteps
  # the inheritance question entirely.
  local i=0
  local entry
  local indexed=()
  for entry in "${ai_entries[@]}"; do
    indexed+=("$i|$entry")
    i=$((i + 1))
  done
  # Probe is non-interactive: </dev/null is a clean EOF for tools that
  # prompt for an API key on first run; `timeout 3` caps the wait.
  # shellcheck disable=SC2016
  local _TO=""
  command -v timeout >/dev/null 2>&1 && _TO="timeout 3 "
  local probe_script='
    payload="$1"; out_dir="$2"; to="$3"
    i="${payload%%|*}"; entry="${payload#*|}"
    IFS="|" read -r category role name bin desc <<<"$entry"
    if command -v "$bin" >/dev/null 2>&1; then
      output=$($to "$bin" --version </dev/null 2>/dev/null | head -1) || true
      version=$(printf "%s" "$output" | sed "s/^[^0-9]*//;s/[[:space:]]*$//;s/\.$//")
      printf "%s\t1\t%s\n" "$bin" "$version" >"$out_dir/$i"
    else
      printf "%s\t0\t\n" "$bin" >"$out_dir/$i"
    fi
  '
  # NOTE: `-I{}` already implies one input line per invocation. Adding
  # `-n1` on top triggers a BSD-xargs quirk where the input line is
  # word-split on whitespace ("0|Agents (autonomous)|..." → multiple
  # entries). Use only `-I{}`.
  #
  # ALSO: feed null-delimited records (`-0`) so apostrophes and
  # other quote-like characters in the descriptions ("Block's coding
  # agent") don't trigger xargs's "unterminated quote" parser.
  printf '%s\0' "${indexed[@]}" |
    xargs -0 -I{} -P"$jobs" \
      bash -c "$probe_script" _ {} "$probe_dir" "$_TO" \
      2>/dev/null || true

  # Re-assemble in original entry order.
  local n
  for ((n = 0; n < i; n++)); do
    [[ -f "$probe_dir/$n" ]] && cat "$probe_dir/$n" >>"$tmp_file"
  done
  rm -rf "$probe_dir"

  # Guard with `|| true` because ui_spinner_stop's last line evaluates
  # to rc=1 when stdout is a TTY (the `[[ ! -t 1 ]] && printf` short-
  # circuit). Under `set -euo pipefail` that rc would kill the script
  # right before we get to write the cache, leaving the user with a
  # silent broken cold-cache run. Defence in depth — the function
  # now also has an explicit `return 0` upstream.
  ui_spinner_stop || true

  mv "$tmp_file" "$AI_STATUS_CACHE_FILE"
}

_ai_get_cached_status() {
  cat "$AI_STATUS_CACHE_FILE"
}

# binary -> mise package mapping
_ai_mise_pkg() {
  case "$1" in
    claude) echo "npm:@anthropic-ai/claude-code" ;;
    codex) echo "npm:@openai/codex" ;;
    copilot) echo "npm:@github/copilot" ;;
    goose) echo "pipx:goose-ai" ;;
    aider) echo "pipx:aider-chat" ;;
    opencode) echo "npm:opencode-ai" ;;
    sgpt) echo "pipx:shell-gpt" ;;
    gemini) echo "npm:@google/gemini-cli" ;;
    ollama) echo "aqua:ollama/ollama" ;;
    kiro-cli) echo "kiro-cli" ;;
    autohand) echo "npm:autohand-cli" ;;
    vibe) echo "pipx:mistral-vibe" ;;
    qwen) echo "npm:@qwen-code/qwen-code" ;;
    zai) echo "npm:@guizmo-ai/zai-cli" ;;
    *) echo "" ;;
  esac
}

cmd_ai_status() {
  ui_header "AI CLI Status"

  # category|role|name|binary|description
  local -a ai_clis=(
    "Agents (autonomous)|agent|Claude Code|claude|Anthropic CLI agent"
    "Agents (autonomous)|agent|Codex CLI|codex|OpenAI Codex agent"
    "Agents (autonomous)|agent|Copilot CLI|copilot|GitHub Copilot CLI"
    "Agents (autonomous)|agent|Goose|goose|Block's coding agent"
    "Coding (interactive)|coding|Aider|aider|AI pair programmer"
    "Coding (interactive)|coding|OpenCode|opencode|Terminal coding assistant"
    "Coding (interactive)|coding|Autohand Code|autohand|Autohand coding agent"
    "Coding (interactive)|coding|Mistral Vibe|vibe|Mistral AI coding agent"
    "Coding (interactive)|coding|Qwen Code|qwen|Qwen AI coding assistant"
    "Coding (interactive)|coding|ZAI|zai|Zhipu AI coding agent"
    "General (prompt-based)|general|Shell-GPT|sgpt|ChatGPT terminal interface"
    "General (prompt-based)|general|Gemini CLI|gemini|Google AI CLI"
    "Runtime (local)|local|Ollama|ollama|Local LLM runner"
    "Cloud (platform)|cloud|Kiro CLI|kiro-cli|AWS AI assistant"
  )

  if ! _ai_cache_fresh "$AI_STATUS_CACHE_FILE"; then
    _ai_refresh_status_cache ai_clis
  fi

  declare -A ai_present=()
  declare -A ai_version=()
  local cached_bin cached_present cached_version
  while IFS=$'\t' read -r cached_bin cached_present cached_version; do
    ai_present["$cached_bin"]="$cached_present"
    ai_version["$cached_bin"]="$cached_version"
  done < <(_ai_get_cached_status)

  local -a installed=()
  local -a missing=()
  local current_category=""
  local category role name bin desc ver
  for entry in "${ai_clis[@]}"; do
    IFS='|' read -r category role name bin desc <<<"$entry"
    if [[ "$category" != "$current_category" ]]; then
      echo ""
      ui_section "$category"
      current_category="$category"
    fi
    if [[ "${ai_present[$bin]:-0}" == "1" ]]; then
      ver="${ai_version[$bin]:-installed}"
      [[ "$bin" == "claude" ]] && ver="${ver%% *}"
      ui_ok "$name" "$ver — $desc"
      installed+=("$name|$bin|$role")
    else
      ui_info "$name" "— $desc (not installed)"
      missing+=("$name|$bin")
    fi
  done

  # Offer to install missing providers via mise
  if [[ ${#missing[@]} -gt 0 ]] && has_command mise; then
    echo ""
    local _ai_install_action=""
    if has_command gum; then
      _ai_install_action=$(printf '%s\n' "Install all" "Choose which to install" "Skip" |
        gum choose --header "Missing AI providers — install via mise?") || _ai_install_action=""
    else
      ui_info "Tip" "Install missing providers: mise install"
      ui_info "Tip" "Or individually: mise use -g <package>@latest"
    fi

    local -a _ai_to_install=()
    case "$_ai_install_action" in
      "Install all")
        _ai_to_install=("${missing[@]}")
        ;;
      "Choose which to install")
        local -a _ai_pick_choices=()
        for entry in "${missing[@]}"; do
          IFS='|' read -r name bin <<<"$entry"
          _ai_pick_choices+=("$name")
        done
        local _ai_picked
        _ai_picked=$(printf '%s\n' "${_ai_pick_choices[@]}" |
          gum choose --no-limit --header "Select providers to install (Space to toggle, Enter to confirm)") || _ai_picked=""
        if [[ -n "$_ai_picked" ]]; then
          while IFS= read -r selected; do
            [[ -z "$selected" ]] && continue
            for entry in "${missing[@]}"; do
              IFS='|' read -r name bin <<<"$entry"
              if [[ "$name" == "$selected" ]]; then
                _ai_to_install+=("$entry")
              fi
            done
          done <<<"$_ai_picked"
        fi
        ;;
    esac

    if [[ ${#_ai_to_install[@]} -gt 0 ]]; then
      echo ""
      for entry in "${_ai_to_install[@]}"; do
        IFS='|' read -r name bin <<<"$entry"
        local pkg
        pkg=$(_ai_mise_pkg "$bin")
        if [[ -n "$pkg" ]]; then
          if has_command gum; then
            if gum spin --spinner dot --title "Installing $name ($pkg)" -- \
              mise use -g "$pkg@latest" 2>&1; then
              ui_ok "$name" "installed"
            else
              ui_warn "$name" "install failed (continuing)"
            fi
          else
            ui_info "Installing" "$name via mise ($pkg)"
            mise use -g "$pkg@latest" 2>&1 || ui_warn "$name" "install failed (continuing)"
          fi
        fi
      done
      # Invalidate cache after installs
      rm -f "$AI_STATUS_CACHE_FILE"
      echo ""
      ui_ok "Done" "Run 'dot ai' again to see updated status"
    fi
  fi

  echo ""
  if [ ${#installed[@]} -eq 0 ]; then
    ui_warn "No AI CLIs installed"
  elif has_command gum; then
    ui_info "Launch" "Select an AI CLI to start"
    local -a choices=()
    for entry in "${installed[@]}"; do
      IFS='|' read -r name bin role <<<"$entry"
      choices+=("$(printf '%-16s — %s' "$name" "$role")")
    done
    local pick
    pick=$(printf '%s\n' "${choices[@]}" | gum choose --header "Select an AI CLI") || true
    if [ -n "$pick" ]; then
      pick="${pick%% — *}"
      pick="${pick%"${pick##*[![:space:]]}"}"
      for entry in "${installed[@]}"; do
        IFS='|' read -r name bin role <<<"$entry"
        if [ "$name" = "$pick" ]; then
          echo ""
          ui_info "Starting" "$name ($bin)"
          exec "$bin"
        fi
      done
    fi
  else
    ui_info "Tip" "Install gum for interactive launcher: mise use -g gum"
  fi
}

cmd_ai_setup() {
  run_script "scripts/ops/ai-setup.sh" "AI setup script" "$@"
}

cmd_ai_query() {
  run_script "dot_local/bin/executable_dot-ai" "AI RAG script" "$@"
}

# Locate the vibe-delegate / delegate-report tools deployed by the
# /vibe Claude Code skill. Path stays in sync with
# defaults/dot_claude/skills/vibe/tools/.
_ai_delegate_tool() {
  local tool="$1" # vibe-delegate | delegate-report
  local path="${HOME}/.claude/skills/vibe/tools/${tool}"
  if [[ ! -x "$path" ]]; then
    # Errors to stderr so callers capturing stdout via $() get just the path.
    ui_err "$tool" "not found at $path" >&2
    ui_info "Hint" "Run 'chezmoi apply' to deploy the /vibe skill" >&2
    return 1
  fi
  printf '%s\n' "$path"
}

cmd_ai_delegate() {
  # Front-door to the same delegator the /vibe slash command uses.
  # Usage: dot ai delegate "<prompt>" [max-turns] [agent] [timeout]
  if [[ $# -lt 1 ]]; then
    ui_err "Usage" "dot ai delegate \"<prompt>\" [max-turns] [agent] [timeout-secs]"
    ui_info "Example" "dot ai delegate \"add a CHANGELOG entry for v0.2.504\""
    return 1
  fi
  local prompt="$1"
  shift
  local max_turns="${1:-10}"
  local agent="${2:-}"
  local timeout="${3:-180}"
  local tool
  tool="$(_ai_delegate_tool vibe-delegate)" || return 1
  if ! has_command vibe; then
    ui_warn "vibe" "not installed"
    ui_info "Install" "mise use -g pipx:mistral-vibe"
    return 1
  fi
  "$tool" "$(pwd)" "$prompt" "$max_turns" "$agent" "$timeout"
}

cmd_ai_cost() {
  # Unified AI spend report. Wraps delegate-report (vibe runs) and is
  # the documented interface for users: path may move later.
  local tool
  tool="$(_ai_delegate_tool delegate-report)" || return 1
  "$tool" "$@"
}

# Append a best-effort run entry to the unified AI log read by
# delegate-report. Token/cost fields are left blank when the provider
# CLI does not surface them — the report tolerates missing fields and
# groups by `model`. This gives users one place to see invocations
# across Claude, Gemini, Aider, Vibe, etc., not just Vibe.
_ai_log_run() {
  local provider="$1" exit_code="$2" duration_secs="$3" prompt_words="$4"
  local log_file="${HOME}/.local/share/delegate-runs.jsonl"
  mkdir -p "$(dirname "$log_file")"
  local project ts
  project="$(basename "$PWD")"
  ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  # python3 keeps the encoding boring; jq would also work but isn't
  # guaranteed present everywhere this script runs.
  python3 - "$log_file" "$provider" "$project" "$exit_code" "$duration_secs" "$prompt_words" "$ts" <<'PY'
import json, sys
log, provider, project, ec, dur, pw, ts = sys.argv[1:8]
entry = {
    "ts": ts,
    "delegate": provider,
    "model": provider,
    "project": project,
    "exit_code": int(ec),
    "duration_secs": float(dur),
    "prompt_words": int(pw),
    "tool_calls": 0,
    "files_changed": 0,
    "wrote_nothing": False,
    "warn_count": 0,
    "search_replace_fails": 0,
    "syntax_errors": 0,
    "tokens_in": 0,
    "tokens_out": 0,
    "tokens_total": 0,
    "cost_usd": 0,
    "cost_claude_eq": 0,
}
with open(log, "a") as fh:
    fh.write(json.dumps(entry) + "\n")
PY
}

run_ai_with_context() {
  local tool="$1"
  shift
  local pattern_name=""
  local prompt=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --help | -h)
        _show_ai_bridge_usage
        exit 0
        ;;
      --pattern | -p)
        pattern_name="$2"
        shift 2
        ;;
      *)
        prompt="$1"
        shift
        ;;
    esac
  done

  if [[ -z "$prompt" ]]; then
    _show_ai_bridge_usage
    exit 1
  fi

  local system_context=""
  if [[ -n "$pattern_name" ]]; then
    local pattern_file="$PATTERN_DIR/${pattern_name}.md"
    if [[ -f "$pattern_file" ]]; then
      system_context=$(cat "$pattern_file")
    else
      ui_err "Pattern not found" "$pattern_name"
      exit 1
    fi
  fi

  # Inject dynamic system metadata
  local metadata
  metadata="## System Metadata
- OS: $(uname -s) $(uname -r)
- Arch: $(uname -m)
- Date: $(date -u)"

  local full_prompt="${system_context}

${metadata}

## User Request
${prompt}"

  # Resolve the binary name for the tool
  local tool_bin="$tool"
  case "$tool" in
    cl) tool_bin="claude" ;;
    kiro) tool_bin="kiro-cli" ;;
  esac

  # Check if the tool is installed; offer mise install if not
  if ! has_command "$tool_bin"; then
    local mise_pkg
    mise_pkg=$(_ai_mise_pkg "$tool_bin")
    if [[ -n "$mise_pkg" ]] && has_command mise; then
      ui_warn "$tool" "not installed"
      local do_install=""
      if has_command gum; then
        do_install=$(gum confirm "Install $tool via mise ($mise_pkg)?" && echo "yes" || echo "no")
      else
        printf "Install %s via mise (%s)? [y/N] " "$tool" "$mise_pkg"
        read -r do_install
        case "$do_install" in y | Y | yes) do_install="yes" ;; *) do_install="no" ;; esac
      fi
      if [[ "$do_install" == "yes" ]]; then
        ui_info "Installing" "$tool via mise ($mise_pkg)"
        mise use -g "$mise_pkg@latest" 2>&1 || {
          ui_err "$tool" "installation failed"
          exit 1
        }
        rm -f "$AI_STATUS_CACHE_FILE"
      else
        ui_err "$tool" "not installed — install with: mise use -g $mise_pkg@latest"
        exit 1
      fi
    else
      ui_err "$tool" "not installed and mise not available"
      exit 1
    fi
  fi

  ui_info "Executing $tool with pattern: ${pattern_name:-none}"

  # Wrap the provider invocation so we can log it to the unified AI run
  # log. Each entry feeds `dot ai cost` so users see spend across every
  # provider, not just vibe. Token-level fields are zero for providers
  # that don't surface them — the report tolerates missing data.
  local _ai_start_ts _ai_exit _ai_end_ts _ai_dur _ai_prompt_words
  _ai_start_ts=$(date +%s)
  _ai_prompt_words=$(printf '%s' "$prompt" | wc -w | tr -d ' ')
  _ai_exit=0
  case "$tool" in
    cl | claude)
      printf "%s" "$full_prompt" | claude || _ai_exit=$?
      ;;
    codex)
      printf "%s" "$full_prompt" | codex || _ai_exit=$?
      ;;
    copilot)
      copilot -sp "$full_prompt" || _ai_exit=$?
      ;;
    gemini)
      printf "%s" "$full_prompt" | gemini chat || _ai_exit=$?
      ;;
    goose)
      printf "%s" "$full_prompt" | goose session start || _ai_exit=$?
      ;;
    kiro | kiro-cli)
      printf "%s" "$full_prompt" | kiro-cli chat || _ai_exit=$?
      ;;
    sgpt)
      printf "%s" "$full_prompt" | sgpt --chat shell-gpt || _ai_exit=$?
      ;;
    ollama)
      printf "%s" "$full_prompt" | ollama run llama3.2 || _ai_exit=$?
      ;;
    opencode)
      printf "%s" "$full_prompt" | opencode query || _ai_exit=$?
      ;;
    aider)
      printf "%s" "$full_prompt" | aider --msg "-" || _ai_exit=$?
      ;;
    autohand)
      printf "%s" "$full_prompt" | autohand chat || _ai_exit=$?
      ;;
    vibe)
      printf "%s" "$full_prompt" | vibe chat || _ai_exit=$?
      ;;
    qwen)
      printf "%s" "$full_prompt" | qwen chat || _ai_exit=$?
      ;;
    zai)
      printf "%s" "$full_prompt" | zai chat || _ai_exit=$?
      ;;
    *)
      ui_err "Unsupported tool" "$tool"
      exit 1
      ;;
  esac
  _ai_end_ts=$(date +%s)
  _ai_dur=$((_ai_end_ts - _ai_start_ts))
  _ai_log_run "$tool_bin" "$_ai_exit" "$_ai_dur" "$_ai_prompt_words" 2>/dev/null || true
  return "$_ai_exit"
}

# Dispatch
case "${1:-}" in
  ai)
    shift
    # Subcommands first: `dot ai delegate`, `dot ai cost`. Fall through
    # to status if no subcommand or unknown one.
    case "${1:-}" in
      delegate)
        shift
        cmd_ai_delegate "$@"
        ;;
      cost)
        shift
        cmd_ai_cost "$@"
        ;;
      "" | status)
        cmd_ai_status "$@"
        ;;
      *)
        cmd_ai_status "$@"
        ;;
    esac
    ;;
  ai-setup)
    shift
    cmd_ai_setup "$@"
    ;;
  ai-query)
    shift
    cmd_ai_query "$@"
    ;;
  cl | claude | codex | copilot | gemini | goose | kiro | sgpt | ollama | opencode | aider | autohand | vibe | qwen | zai)
    tool="$1"
    shift
    run_ai_with_context "$tool" "$@"
    ;;
  *)
    echo "Unknown ai command: ${1:-}" >&2
    exit 1
    ;;
esac
