#!/usr/bin/env bash
# lib/git-command-classifier.sh — the Git Command Classifier: given the locked
# branch and a shell command string, decides whether the command would lose the
# agent's work while a lock is active and must be blocked. Pure / explicit-args,
# like the sibling lib/ modules. Self-contained: it carries its own small
# command matcher so the branch-lock hook needs no dependency on the
# git-guardrails skill.
#
# It blocks two families of command:
#   1. Branch-leaving `git checkout <branch>` / `git switch <branch>` (the
#      original slice) — unless the target is the lock branch itself.
#   2. The work-loss family (issue #61): commands that throw away the working
#      tree or the stash —
#        - `git stash` / `git stash push` / `git stash save`  (bare stash defaults to push)
#        - `git clean` with any force flag (`-f`, `-fd`, `-xfd`, `--force`)
#        - `git reset --hard`
#        - whole-tree restore: `git checkout .`, `git checkout -- .`, `git restore .`
#
# Everything else is allowed — switching back to the lock branch, targeted
# single-file restore (`git checkout -- <path>`, `git restore <path>`), a bare
# `git checkout`, read-only stash (`list`/`show`), a dry-run clean (`-n`), a
# soft/mixed reset, an unstage (`git restore --staged`), `git worktree add`
# (worktrees are how /afk works and are exempt by scope), and any other command.
#
# Recognition is intentionally conservative: it scans the token stream for a
# `git` token, skips any global options between `git` and the subcommand
# (`git -C <path> checkout x`, `git --git-dir=… switch y`, `git -c k=v …`), and
# classifies the recognised subcommand, so a compound command
# (`cd x && git reset --hard`) and a global-option form are both classified.
#
# Public surface:
#   classify_git_command <lock_branch> <command>
#     Echoes exactly "block" or "allow" and returns 0.
#
#   classify_primary_branch_switch_guard <command>
#     Echoes exactly "block" for commands that would move or destroy work in the
#     primary checkout, and "allow" otherwise. This is the config-flag guard
#     from ADR 0043 (unlike classify_git_command it has no lock branch). It
#     blocks: branch movement (switch/create/checkout to a branch); `git reset`
#     in ANY form; every `git stash` subcommand; and `git rebase --autostash`
#     (issue #1024). Parallel human WIP lives in the primary checkout and these
#     commands have destroyed in-progress work before; do branch work in an
#     isolated worktree under .red/tmp/ instead. Worktrees are exempt by scope,
#     so this classifier only ever runs against the primary checkout.

# _git_subcommand_index <toks-name> <git-index>
# Given the name of the token array and the index of a `git` token, echoes the
# index of the git subcommand, skipping any leading global options. Global
# options that take a separate argument (`-C <path>`, `-c <name=value>`,
# `--git-dir <path>`, `--work-tree <path>`, `--namespace <name>`,
# `--exec-path <path>`, `--super-prefix <path>`) consume the following token;
# the `--opt=value` form is self-contained, and bare flags (`-p`, `--no-pager`,
# `--bare`, …) consume only themselves. Echoes the array length when no
# subcommand follows.
_git_subcommand_index() {
  local -n _t="$1"
  local k=$(( $2 + 1 ))
  local len=${#_t[@]}
  while (( k < len )); do
    local tok="${_t[k]}"
    case "$tok" in
      -C|-c|--git-dir|--work-tree|--namespace|--exec-path|--super-prefix)
        k=$(( k + 2 )); continue ;;   # option + its separate argument
      --*=*|-*)
        k=$(( k + 1 )); continue ;;   # `--opt=value` or a bare flag
      *)
        break ;;                      # the subcommand token
    esac
  done
  echo "$k"
}

# classify_git_command <lock_branch> <command>
classify_git_command() {
  local _lock="$1" _cmd="$2"
  local -a toks
  read -ra toks <<<"$_cmd"
  local n=${#toks[@]} i j

  for ((i = 0; i < n; i++)); do
    [[ "${toks[i]}" == "git" ]] || continue
    local s; s="$(_git_subcommand_index toks "$i")"   # subcommand index, past global options
    local sub="${toks[s]:-}"
    case "$sub" in
      worktree)
        echo "allow"; return 0
        ;;
      stash)
        # bare `git stash` defaults to push; push/save discard the working tree.
        local op="${toks[s + 1]:-}"
        if [[ -z "$op" || "$op" == "push" || "$op" == "save" ]]; then
          echo "block"; return 0
        fi
        echo "allow"; return 0   # list / show / pop / apply / drop …
        ;;
      clean)
        # any force flag makes clean destructive; -n/--dry-run is safe.
        for ((j = s + 1; j < n; j++)); do
          local t="${toks[j]}"
          [[ "$t" == "--force" ]] && { echo "block"; return 0; }
          # short flag bundle containing 'f' (-f, -fd, -xfd, …) but not -n.
          if [[ "$t" == -[^-]* && "$t" == *f* ]]; then echo "block"; return 0; fi
        done
        echo "allow"; return 0
        ;;
      reset)
        for ((j = s + 1; j < n; j++)); do
          [[ "${toks[j]}" == "--hard" ]] && { echo "block"; return 0; }
        done
        echo "allow"; return 0
        ;;
      restore)
        # whole-tree restore (`git restore .`) loses work; a targeted path or an
        # unstage (`--staged <path>`) is allowed, mirroring `checkout -- <path>`.
        for ((j = s + 1; j < n; j++)); do
          local t="${toks[j]}"
          [[ "$t" == -* ]] && continue   # skip flags (--staged, --source=…, …)
          if [[ "$t" == "." ]]; then echo "block"; return 0; fi
          break                          # a named path: allowed
        done
        echo "allow"; return 0
        ;;
      checkout|switch)
        local target="" sawdoubledash=0
        for ((j = s + 1; j < n; j++)); do
          local t="${toks[j]}"
          if [[ "$t" == "--" ]]; then sawdoubledash=1; continue; fi
          if [[ "$t" == "-" ]]; then target="-"; break; fi  # `switch -` = previous branch
          [[ "$t" == -* ]] && continue   # skip flags (-b, -c, -q, --track, …)
          target="$t"; break
        done
        # whole-tree restore via checkout (`git checkout .`, `git checkout -- .`).
        if [[ "$target" == "." ]]; then echo "block"; return 0; fi
        if (( sawdoubledash )); then echo "allow"; return 0; fi   # file restore
        if [[ -z "$target" ]]; then echo "allow"; return 0; fi    # bare checkout
        if [[ "$target" == "$_lock" ]]; then echo "allow"; return 0; fi # back to lock
        echo "block"; return 0
        ;;
    esac
  done

  echo "allow"; return 0
}

# classify_primary_branch_switch_guard <command>
classify_primary_branch_switch_guard() {
  local _cmd="$1"
  local -a toks
  read -ra toks <<<"$_cmd"
  local n=${#toks[@]} i j

  for ((i = 0; i < n; i++)); do
    [[ "${toks[i]}" == "git" ]] || continue
    local s; s="$(_git_subcommand_index toks "$i")"   # subcommand index, past global options
    local sub="${toks[s]:-}"
    case "$sub" in
      worktree)
        echo "allow"; return 0
        ;;
      reset)
        # `git reset` in ANY form (--hard/--soft/--mixed, with or without a
        # pathspec) can throw away parallel human WIP in the primary checkout
        # (issue #1024). All forms blocked; use a worktree for branch work.
        echo "block"; return 0
        ;;
      stash)
        # `git stash` and every subcommand (push/save/pop/apply/list/show/…)
        # blocked in the primary checkout (issue #1024) — the whole family
        # touches or hides the primary's working tree.
        echo "block"; return 0
        ;;
      rebase)
        # `git rebase --autostash` silently stashes/pops the primary's WIP
        # around the rebase (issue #1024); a plain rebase is not this vector.
        for ((j = s + 1; j < n; j++)); do
          [[ "${toks[j]}" == "--autostash" ]] && { echo "block"; return 0; }
        done
        echo "allow"; return 0
        ;;
      checkout|switch)
        local target="" sawdoubledash=0
        for ((j = s + 1; j < n; j++)); do
          local t="${toks[j]}"
          if [[ "$t" == "--" ]]; then sawdoubledash=1; continue; fi
          if [[ "$t" == "-" ]]; then target="-"; break; fi
          [[ "$t" == -* ]] && continue
          target="$t"; break
        done
        if (( sawdoubledash )); then echo "allow"; return 0; fi
        if [[ -z "$target" ]]; then echo "allow"; return 0; fi
        if [[ "$target" == "." ]]; then echo "allow"; return 0; fi
        echo "block"; return 0
        ;;
    esac
  done

  echo "allow"; return 0
}
