#!/bin/bash
set -o pipefail
# git-ai - LLM-powered git workflow tools
# Usage: git-ai <commit|pr|setup|providers|models|options> [...]

SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=lib/ai-common.sh
source "${SCRIPT_DIR}/../lib/ai-common.sh"
# shellcheck source=lib/setup.sh
source "${SCRIPT_DIR}/../lib/setup.sh"

# ---------------------------------------------------------------------------
# PR cache helpers
# ---------------------------------------------------------------------------

current_branch_name() {
  local branch
  branch=$(git branch --show-current 2>/dev/null) || return 1
  [[ -n "$branch" ]] || return 1
  printf '%s\n' "$branch"
}

branch_cache_path() {
  local git_dir="$1"
  local branch_name="$2"
  local base_branch="$3"
  "${GIT_AI_PYTHON:-python3}" - <<'PY' "$SCRIPT_DIR" "$git_dir" "$branch_name" "$base_branch" || return 1
import sys
from pathlib import Path

script_dir, git_dir, branch_name, base_branch = sys.argv[1:]
sys.path.insert(0, str(Path(script_dir).resolve().parent / "python" / "git_ai"))
from _pr_incremental import branch_cache_path

print(branch_cache_path(git_dir, branch_name, base_branch))
PY
}

load_cached_pr() {
  local git_dir="$1"
  local branch_name="$2"
  local base_branch="$3"
  local cache_file
  cache_file=$(branch_cache_path "$git_dir" "$branch_name" "$base_branch") || return 1
  [[ -r "$cache_file" ]] || return 1
  cat "$cache_file"
}

load_cached_pr_sha() {
  local git_dir="$1"
  local branch_name="$2"
  local base_branch="$3"
  local cache_file sha_file
  cache_file=$(branch_cache_path "$git_dir" "$branch_name" "$base_branch") || return 1
  sha_file="$(dirname "$cache_file")/last-head-sha"
  [[ -r "$sha_file" ]] || return 1
  cat "$sha_file"
}

save_cached_pr() {
  local git_dir="$1"
  local branch_name="$2"
  local base_branch="$3"
  local output="$4"
  local head_sha="${5:-}"
  local output_file
  output_file=$(mktemp) || return 1
  printf '%s\n' "$output" >"$output_file"
  local save_cmd=(
    "${GIT_AI_PYTHON:-python3}" "${SCRIPT_DIR}/../python/git_ai/_pr_repo_cli.py" save-cache
    --git-dir "$git_dir"
    --branch-name "$branch_name"
    --base-branch "$base_branch"
    --output-file "$output_file"
  )
  if [[ -n "$head_sha" ]]; then
    save_cmd+=(--head-sha "$head_sha")
  fi
  "${save_cmd[@]}" || {
      rm -f "$output_file"
      return 1
    }
  rm -f "$output_file"
}

render_pr_output() {
  local existing="$1"
  local output="$2"
  local old_tmp new_tmp
  old_tmp=$(mktemp 2>/dev/null) || { printf '%s\n' "$output"; return 0; }
  new_tmp=$(mktemp 2>/dev/null) || { rm -f "$old_tmp"; printf '%s\n' "$output"; return 0; }
  printf '%s\n' "$existing" > "$old_tmp"
  printf '%s\n' "$output"   > "$new_tmp"
  "${GIT_AI_PYTHON:-python3}" "${SCRIPT_DIR}/../python/git_ai/_pr_render.py" \
    "$old_tmp" "$new_tmp" \
    || printf '%s\n' "$output"
  rm -f "$old_tmp" "$new_tmp"
}

# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------

cmd_commit() {
  local PROVIDER MODEL BASE=""
  local -a rest=()
  # Pull --base out of the args; everything else stays positional.
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --base)
        BASE="$2"
        shift 2
        ;;
      *)
        rest+=("$1")
        shift
        ;;
    esac
  done
  set -- ${rest[@]+"${rest[@]}"}
  PROVIDER="${1:-}"
  MODEL="${2:-}"
  # Accept single-arg "provider:model" form (used by the lazygit fzf flow).
  if [[ -z "$MODEL" && "$PROVIDER" == *:* ]]; then
    MODEL="${PROVIDER#*:}"
    PROVIDER="${PROVIDER%%:*}"
  fi
  local PROMPT
  PROMPT=$(cat "${SCRIPT_DIR}/../python/git_ai/prompts/commit.txt") || die "failed to read commit prompt"

  git rev-parse --git-dir >/dev/null 2>&1 || die "not inside a git repository"

  [[ -z "$PROVIDER" ]] && maybe_first_run_setup commit

  if [[ -z "$PROVIDER" ]]; then
    local IS_TTY=false PICKED
    [[ -t 1 ]] && IS_TTY=true
    PICKED=$(pick_or_recall_provider commit "$IS_TTY") || \
      die "no saved auth method; run 'git-ai providers commit' and then 'git-ai models <auth-method> commit'"
    if [[ "$PICKED" == *:* ]]; then
      PROVIDER="${PICKED%%:*}"
      MODEL="${PICKED#*:}"
    else
      PROVIDER="$PICKED"
    fi
  fi
  provider_is_valid "$PROVIDER" || die "unknown auth method: $PROVIDER"

  if [[ "$PROVIDER" == "last" ]]; then
    local GIT_DIR
    GIT_DIR=$(git rev-parse --git-dir)
    [[ -r "${GIT_DIR}/commit-last-message" ]] || die "no saved commit message"
    push_choice_history commit "last"
    cat "${GIT_DIR}/commit-last-message"
    return 0
  fi

  local REPO_ROOT
  REPO_ROOT=$(git rev-parse --show-toplevel) || die "failed to determine repo root"

  local -a IGNORE_PATTERNS=() PATHSPEC=()
  local _p
  while IFS= read -r _p; do
    [[ -n "$_p" ]] && IGNORE_PATTERNS+=("$_p")
  done < <(load_git_ai_ignore "$REPO_ROOT")
  while IFS= read -r _p; do
    [[ -n "$_p" ]] && PATHSPEC+=("$_p")
  done < <(build_pathspec_excludes ${IGNORE_PATTERNS[@]+"${IGNORE_PATTERNS[@]}"})

  git diff --staged --quiet ${PATHSPEC[@]+"${PATHSPEC[@]}"} && die "no staged changes to summarize"

  local STAGED_DIFF LAST_TAG COMMITS_SINCE RELEASE_CONTEXT INPUT generated
  STAGED_DIFF=$(git diff --staged ${PATHSPEC[@]+"${PATHSPEC[@]}"}) || die "failed to read staged diff"

  check_diff_size_or_die "$STAGED_DIFF"

  LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
  if [[ -n "$LAST_TAG" ]]; then
    COMMITS_SINCE=$(git rev-list --count "${LAST_TAG}..HEAD" 2>/dev/null || echo "?")
    RELEASE_CONTEXT="Release context: last tag ${LAST_TAG}, ${COMMITS_SINCE} commits since — staged changes are unreleased"
  else
    RELEASE_CONTEXT="Release context: no release tags found — treat all changes as unreleased"
  fi

  # Best-effort branch context so the prefix is chosen from the perspective of
  # the whole branch. Never blocks a commit: failures yield an empty block.
  local BRANCH_CONTEXT
  local -a bc_cmd=(
    "${GIT_AI_PYTHON:-python3}" "${SCRIPT_DIR}/../python/git_ai/_commit_cli.py"
    branch-context --repo "$REPO_ROOT"
  )
  # Only --base is forwarded here; GIT_AI_COMMIT_BASE is consumed inside Python
  # (_emit_branch_context), so it still applies when --base isn't passed.
  [[ -n "$BASE" ]] && bc_cmd+=(--base "$BASE")
  BRANCH_CONTEXT=$("${bc_cmd[@]}" 2>/dev/null) || BRANCH_CONTEXT=""

  INPUT="<release_context>${RELEASE_CONTEXT}</release_context>"
  if [[ -n "$BRANCH_CONTEXT" ]]; then
    INPUT+=$'\n\n'"${BRANCH_CONTEXT}"
  fi
  INPUT+=$'\n\n'"<diff>
${STAGED_DIFF}
</diff>"

  generated=$(run_provider commit "$PROVIDER" "$PROMPT" "$INPUT" "$MODEL") || exit $?
  save_last_message commit "$generated"
  printf '%s\n' "$generated"
}

cmd_pr() {
  local PROVIDER MODEL BASE FRESH FROM_SHA
  PROVIDER=""
  MODEL=""
  BASE=""
  FRESH=false
  FROM_SHA=""

  while [[ $# -gt 0 ]]; do
    case $1 in
      --base)
        BASE="$2"
        shift 2
        ;;
      --fresh)
        FRESH=true
        shift
        ;;
      --from-sha)
        FROM_SHA="$2"
        shift 2
        ;;
      vertex-gemini|vertex-anthropic|gemini-api|claude-code|anthropic-api|codex|openai-api)
        PROVIDER="$1"
        shift
        # Accept any non-flag token as the model ID; resolve_model validates
        # against both built-ins and custom entries in options.conf.
        if [[ -n "${1:-}" && "$1" != --* ]]; then
          MODEL="$1"
          shift
        fi
        ;;
      vertex-gemini:*|vertex-anthropic:*|gemini-api:*|claude-code:*|anthropic-api:*|codex:*|openai-api:*)
        PROVIDER="${1%%:*}"
        MODEL="${1#*:}"
        shift
        ;;
      vertex-gemini@*|vertex-anthropic@*)
        # Profile-qualified vertex provider, optionally with :model appended.
        if [[ "$1" == *:* ]]; then
          PROVIDER="${1%%:*}"
          MODEL="${1#*:}"
          shift
        else
          PROVIDER="$1"
          shift
          if [[ -n "${1:-}" && "$1" != --* ]]; then
            MODEL="$1"
            shift
          fi
        fi
        ;;
      *)
        die "unknown argument: $1"
        ;;
    esac
  done

  [[ -z "$PROVIDER" ]] && maybe_first_run_setup pr

  if [[ -z "$PROVIDER" ]]; then
    local IS_TTY=false PICKED
    [[ -t 1 ]] && IS_TTY=true
    PICKED=$(pick_or_recall_provider pr "$IS_TTY") || \
      die "no saved auth method; run 'git-ai providers pr' and then 'git-ai models <auth-method> pr'"
    if [[ "$PICKED" == *:* ]]; then
      PROVIDER="${PICKED%%:*}"
      MODEL="${PICKED#*:}"
    else
      PROVIDER="$PICKED"
    fi
  fi

  local PROMPT_TWO_PASS PROMPT_FALLBACK PROMPT_TWO_PASS_UPDATE PROMPT_FALLBACK_UPDATE
  PROMPT_TWO_PASS=$(cat "${SCRIPT_DIR}/../python/git_ai/prompts/pr-two-pass.txt") || die "failed to read pr-two-pass prompt"
  PROMPT_FALLBACK=$(cat "${SCRIPT_DIR}/../python/git_ai/prompts/pr-fallback.txt") || die "failed to read pr-fallback prompt"
  PROMPT_TWO_PASS_UPDATE=$(cat "${SCRIPT_DIR}/../python/git_ai/prompts/pr-two-pass-update.txt") || die "failed to read pr-two-pass-update prompt"
  PROMPT_FALLBACK_UPDATE=$(cat "${SCRIPT_DIR}/../python/git_ai/prompts/pr-fallback-update.txt") || die "failed to read pr-fallback-update prompt"

  git rev-parse --git-dir >/dev/null 2>&1 || die "not inside a git repository"
  local GIT_DIR
  GIT_DIR=$(git rev-parse --git-dir)

  if [[ -z "$BASE" ]]; then
    BASE=$(git remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p')
    BASE=${BASE:-main}
  fi
  local PREP_OUTPUT PREP_FILE
  PREP_FILE=$(mktemp) || die "failed to allocate temp file"
  local prep_cmd=(
    "${GIT_AI_PYTHON:-python3}" "${SCRIPT_DIR}/../python/git_ai/_pr_repo_cli.py" prepare
    --repo-path "."
    --base-branch "$BASE"
    --format shell
  )
  if [[ "$FRESH" == "true" ]]; then
    prep_cmd+=(--fresh)
  fi
  if [[ -n "$FROM_SHA" ]]; then
    prep_cmd+=(--previous-head-sha "$FROM_SHA")
  fi
  PREP_OUTPUT=$("${prep_cmd[@]}") || {
      rm -f "$PREP_FILE"
      die "failed to prepare PR context"
    }
  printf '%s\n' "$PREP_OUTPUT" >"$PREP_FILE"
  # Variables set by sourced prep file (BASE_BRANCH/INPUT_BASE consumed but not referenced directly)
  local CURRENT_BRANCH HEAD_SHA EXISTING_PR COMMIT_LOG DIFF DIFF_STAT RELEASE_CONTEXT NO_CHANGES CHURN_SUBJECTS WARNINGS
  # shellcheck disable=SC1090
  source "$PREP_FILE"
  rm -f "$PREP_FILE"

  if [[ -n "${WARNINGS:-}" ]]; then
    while IFS= read -r _warning; do
      [[ -n "$_warning" ]] && echo "git-ai: warning: $_warning" >&2
    done <<<"$WARNINGS"
  fi

  if [[ "$NO_CHANGES" == "true" ]]; then
    echo "git-ai: no new commits since last generation, using cached output (use --fresh to regenerate)" >&2
    printf '%s\n' "$EXISTING_PR"
    return 0
  fi

  check_diff_size_or_die "$DIFF"

  local BUILD_FILE PROMPT_NAME PROMPT INPUT DIFF_FILE LOG_FILE STAT_FILE RELEASE_FILE EXISTING_FILE CHURN_FILE
  BUILD_FILE=$(mktemp) || die "failed to allocate temp file"
  DIFF_FILE=$(mktemp) || die "failed to allocate temp file"
  LOG_FILE=$(mktemp) || die "failed to allocate temp file"
  STAT_FILE=$(mktemp) || die "failed to allocate temp file"
  RELEASE_FILE=$(mktemp) || die "failed to allocate temp file"
  EXISTING_FILE=$(mktemp) || die "failed to allocate temp file"
  CHURN_FILE=$(mktemp) || die "failed to allocate temp file"
  printf '%s' "$DIFF" >"$DIFF_FILE"
  printf '%s' "$COMMIT_LOG" >"$LOG_FILE"
  printf '%s' "$DIFF_STAT" >"$STAT_FILE"
  printf '%s' "$RELEASE_CONTEXT" >"$RELEASE_FILE"
  printf '%s' "$EXISTING_PR" >"$EXISTING_FILE"
  printf '%s' "$CHURN_SUBJECTS" >"$CHURN_FILE"
  "${GIT_AI_PYTHON:-python3}" "${SCRIPT_DIR}/../python/git_ai/_pr_repo_cli.py" build-input \
    --diff-file "$DIFF_FILE" \
    --commit-log-file "$LOG_FILE" \
    --diff-stat-file "$STAT_FILE" \
    --release-context-file "$RELEASE_FILE" \
    --existing-pr-file "$EXISTING_FILE" \
    --churn-subjects-file "$CHURN_FILE" >"$BUILD_FILE" || {
      rm -f "$BUILD_FILE" "$DIFF_FILE" "$LOG_FILE" "$STAT_FILE" "$RELEASE_FILE" "$EXISTING_FILE" "$CHURN_FILE"
      die "failed to build PR prompt input"
    }
  PROMPT_NAME=$("${GIT_AI_PYTHON:-python3}" - <<'PY' "$BUILD_FILE"
import json, sys
from pathlib import Path
print(json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["prompt_name"])
PY
)
  INPUT=$("${GIT_AI_PYTHON:-python3}" - <<'PY' "$BUILD_FILE"
import json, sys
from pathlib import Path
print(json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["user_input"])
PY
)
  rm -f "$BUILD_FILE" "$DIFF_FILE" "$LOG_FILE" "$STAT_FILE" "$RELEASE_FILE" "$EXISTING_FILE" "$CHURN_FILE"

  # "verbatim": the branch is a single Conventional-Commits commit, so its
  # message already IS the best PR summary — use it directly and skip the LLM.
  if [[ "$PROMPT_NAME" == "verbatim" ]]; then
    echo "git-ai: single conventional commit — using it verbatim (no LLM call)" >&2
    save_last_message pr "$INPUT"
    if [[ -n "$CURRENT_BRANCH" ]]; then
      save_cached_pr "$GIT_DIR" "$CURRENT_BRANCH" "$BASE" "$INPUT" "$HEAD_SHA" ||
        die "failed to save PR cache for branch ${CURRENT_BRANCH} against ${BASE}"
    fi
    render_pr_output "${EXISTING_PR:-}" "$INPUT"
    return 0
  fi

  case "$PROMPT_NAME" in
    pr-two-pass.txt) PROMPT="$PROMPT_TWO_PASS" ;;
    pr-fallback.txt) PROMPT="$PROMPT_FALLBACK" ;;
    pr-two-pass-update.txt) PROMPT="$PROMPT_TWO_PASS_UPDATE" ;;
    pr-fallback-update.txt) PROMPT="$PROMPT_FALLBACK_UPDATE" ;;
    *) die "unknown prompt selected: $PROMPT_NAME" ;;
  esac

  if [[ -n "${EXISTING_PR:-}" && -n "${CURRENT_BRANCH:-}" ]]; then
    echo "git-ai: updating existing PR draft for ${CURRENT_BRANCH} (use --fresh to regenerate)" >&2
  fi

  local OUTPUT
  OUTPUT=$(run_provider pr "$PROVIDER" "$PROMPT" "$INPUT" "$MODEL") || exit $?
  save_last_message pr "$OUTPUT"
  if [[ -n "$CURRENT_BRANCH" ]]; then
    save_cached_pr "$GIT_DIR" "$CURRENT_BRANCH" "$BASE" "$OUTPUT" "$HEAD_SHA" ||
      die "failed to save PR cache for branch ${CURRENT_BRANCH} against ${BASE}"
  fi
  render_pr_output "${EXISTING_PR:-}" "$OUTPUT"
}

cmd_providers() {
  list_providers "${1:-commit}"
}

cmd_models() {
  list_models "${1:-}" "${2:-commit}"
}

cmd_options() {
  list_options "${1:-commit}"
}


cmd_setup() {
  printf '\ngit-ai setup\n'
  printf '============\n\n'

  _setup_check_shadow

  local conf
  conf=$(user_options_path)
  if [[ -e "$conf" ]]; then
    _setup_edit_existing "$conf"
  else
    _setup_fresh "$conf"
  fi
}

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  CMD="${1:-}"
  shift || true

  case "$CMD" in
    commit)    cmd_commit "$@" ;;
    pr|mr)     cmd_pr "$@" ;;
    setup)     cmd_setup "$@" ;;
    providers) cmd_providers "$@" ;;
    models)    cmd_models "$@" ;;
    options)   cmd_options "$@" ;;
    tiers)     die "'tiers' has been removed; use 'models' instead" ;;
    "")        die "usage: git-ai <commit|pr|setup|providers|models|options> [...]" ;;
    *)         die "unknown command: $CMD" ;;
  esac
fi
