#!/usr/bin/env bash
# Git wrapper for TeleClaude sessions.
#
# Pure bash — no Python, no external deps. Must be fast (<5ms overhead)
# because shell prompts and hub call git dozens of times per second.
#
# Three-tier permission model for agent sessions:
#   DESTRUCTIVE  — blocked for all agent roles, no override
#   PROTECTED    — integrator only (stash, commit on main, push on main)
#   RESTRICTED   — orchestrator cannot commit on any branch
#
# Commit-producing subcmds (commit, cherry-pick, rebase, merge) are gated:
#   commit (any branch)              → use 'telec code commit'. No env-var
#                                      bypass exists on the commit branch.
#   cherry-pick/rebase/merge on main → integrator role bypass, otherwise
#                                       set TELECLAUDE_OVERRIDE_APPROVED=1
#   cherry-pick/rebase/merge on other branches → pass through
#
# Identity source: $TMPDIR/teleclaude_identity.json (written by daemon).
# Fields: system_role, human_role, session_id.
#
# Behavior by context:
#   Outside tmux ($TMUX unset): pass through, no restrictions.
#   Inside tmux, human session (human_role set, no system_role): worker restrictions.
#   Inside tmux, agent session (system_role set): role-based restrictions.
#   TELECLAUDE_SYSTEM_ROLE env override: honored only when human_role is present.
#
# Installed via `telec projects init` as a symlink at ~/.teleclaude/bin/git.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WRAPPER_LOG="/var/log/instrukt-ai/teleclaude/git-wrapper.log"
IDENTITY_FILENAME="teleclaude_identity.json"

# Use gdate when available so timestamps match the daemon log format
# (ms-precision ISO-8601 Z). Fall back to BSD date (s-precision).
if command -v gdate >/dev/null 2>&1; then
    _ts_cmd() { gdate -u +%Y-%m-%dT%H:%M:%S.%3NZ; }
else
    _ts_cmd() { date -u +%Y-%m-%dT%H:%M:%SZ; }
fi

_find_real_git_fast() {
    IFS=: read -ra path_parts <<< "$PATH"
    for dir in "${path_parts[@]}"; do
        [ "$dir" = "$SCRIPT_DIR" ] && continue
        [ -z "$dir" ] && continue
        if [ -x "$dir/git" ]; then
            echo "$dir/git"
            return 0
        fi
    done
    echo "ERROR: real git binary not found in PATH" >&2
    exit 1
}

# ── Fast exit: not in tmux → no restrictions ─────────────────────────────────
if [ -z "${TMUX:-}" ]; then
    exec "$(_find_real_git_fast)" "$@"
fi

# Declare _real_git_bin once for the session
_real_git_bin="$(_find_real_git_fast)"

_real_git() {
    # Disable all auto-stash for every git invocation through the wrapper.
    local _count="${GIT_CONFIG_COUNT:-0}"
    export GIT_CONFIG_COUNT=$((_count + 2))
    export "GIT_CONFIG_KEY_${_count}=rebase.autoStash"
    export "GIT_CONFIG_VALUE_${_count}=false"
    export "GIT_CONFIG_KEY_$((_count + 1))=merge.autoStash"
    export "GIT_CONFIG_VALUE_$((_count + 1))=false"

    # Per-invocation id correlates start, stdout, stderr, and rc lines in
    # WRAPPER_LOG across concurrent git invocations.
    local _inv_id="$$.$(date +%s).$RANDOM"
    _log_wrapper "$_inv_id" "$@"

    # Save original stdout/stderr so the process-substitution children can
    # pass git output through to the caller while also appending it to
    # WRAPPER_LOG, line-prefixed and timestamped in the same format as the
    # start/end lines.
    exec 3>&1 4>&2
    "$_real_git_bin" "$@" \
        > >(while IFS= read -r _line || [ -n "$_line" ]; do
                echo "$(_ts_cmd) level=INFO logger=git-wrapper inv=$_inv_id stream=stdout msg=$_line" >> "$WRAPPER_LOG"
                printf '%s\n' "$_line" >&3
            done) \
        2> >(while IFS= read -r _line || [ -n "$_line" ]; do
                echo "$(_ts_cmd) level=INFO logger=git-wrapper inv=$_inv_id stream=stderr msg=$_line" >> "$WRAPPER_LOG"
                printf '%s\n' "$_line" >&4
            done)
    local _rc=$?
    exec 3>&- 4>&-

    _log_wrapper_end "$_inv_id" "$_rc"
    return $_rc
}

_log_wrapper() {
    local _inv_id="$1"
    shift
    { echo "$(_ts_cmd) level=INFO logger=git-wrapper inv=$_inv_id event=start role=$_role subcmd=$_subcmd args=$* cwd=$(pwd)" >> "$WRAPPER_LOG"; } 2>/dev/null || true
}

_log_wrapper_end() {
    local _inv_id="$1"
    local _rc="$2"
    local _level=INFO
    [ "$_rc" -ne 0 ] && _level=ERROR
    { echo "$(_ts_cmd) level=$_level logger=git-wrapper inv=$_inv_id event=end rc=$_rc" >> "$WRAPPER_LOG"; } 2>/dev/null || true
}

# ── Read identity from JSON file ─────────────────────────────────────────────
_identity_file="${TMPDIR:-}/${IDENTITY_FILENAME}"
_system_role=""
_human_role=""

if [ -f "$_identity_file" ]; then
    # Minimal JSON parsing with sed — no jq dependency.
    _system_role="$(sed -n 's/.*"system_role"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_identity_file" 2>/dev/null || true)"
    _human_role="$(sed -n 's/.*"human_role"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_identity_file" 2>/dev/null || true)"
fi

# Env var override — only honored when a human owns this session.
if [ -n "$_human_role" ] && [ -n "${TELECLAUDE_SYSTEM_ROLE:-}" ]; then
    _system_role="$TELECLAUDE_SYSTEM_ROLE"
fi

# No identity file or empty — fail fast (broken session).
if [ -z "$_system_role" ] && [ -z "$_human_role" ]; then
    echo "ERROR: No system_role or human_role in ${_identity_file}. Session identity is broken." >&2
    exit 1
fi

# Human in tmux without explicit system_role → apply worker restrictions.
if [ -n "$_human_role" ] && [ -z "$_system_role" ]; then
    _system_role="worker"
fi

_role="$_system_role"

# ── Parse git subcommand and -C args ─────────────────────────────────────────
_subcmd=""
_skip_next=0
_capture_C=0
_dir_args=()
for _arg in "$@"; do
    if [ "$_capture_C" -eq 1 ]; then
        _dir_args+=("-C" "$_arg")
        _capture_C=0
        _skip_next=0
        continue
    fi
    if [ "$_skip_next" -eq 1 ]; then
        _skip_next=0
        continue
    fi
    case "$_arg" in
        -C)
            _capture_C=1
            _skip_next=1
            ;;
        -c|--git-dir|--work-tree|--namespace)
            _skip_next=1
            ;;
        --git-dir=*|--work-tree=*|--namespace=*|-c*)
            ;;
        --bare|--no-pager|--paginate|--no-replace-objects|--literal-pathspecs|--glob-pathspecs|--no-optional-locks)
            ;;
        -*)
            ;;
        *)
            _subcmd="$_arg"
            break
            ;;
    esac
done

# ── Human session without tmux override: directory scope only ─────────────────
# (This path is unreachable now — humans outside tmux exec'd above, humans
#  inside tmux get _system_role=worker. Kept for defensive completeness.)
if [ -n "$_human_role" ] && [ "$_role" = "" ]; then
    _real_git "$@"
    exit $?
fi

# ── DESTRUCTIVE: blocked for all roles, no override ──────────────────────────
case "$_subcmd" in
    restore|clean|switch)
        echo "ERROR: 'git $_subcmd' is a destructive operation and is prohibited." >&2
        exit 1
        ;;
    revert)
        _has_confirmed=0
        for _rarg in "$@"; do
            [ "$_rarg" = "--confirmed" ] && _has_confirmed=1
        done
        if [ "$_has_confirmed" -eq 0 ]; then
            cat >&2 <<'MSG'
WARNING: 'git revert' blocked — inspection required.

Commits in multi-agent environments often contain changes from multiple workers.
A blind revert destroys other agents' work.

Before reverting:
  1. Run: git show --stat <commit>
  2. Verify EVERY file in the commit belongs to the scope you intend to undo.
  3. Rerun with: git revert --confirmed <commit>
MSG
            exit 1
        fi
        _revert_args=()
        for _rarg in "$@"; do
            [ "$_rarg" = "--confirmed" ] && continue
            _revert_args+=("$_rarg")
        done
        _real_git "${_revert_args[@]}"
        exit $?
        ;;
esac

# Destructive reset variants blocked; pathspec reset allowed.
if [ "$_subcmd" = "reset" ]; then
    _has_separator=0
    _non_flag_args=0
    _is_subcmd=0
    for _arg in "$@"; do
        if [ "$_is_subcmd" -eq 0 ]; then
            [ "$_arg" = "reset" ] && _is_subcmd=1
            continue
        fi
        case "$_arg" in
            --hard|--merge|--keep)
                echo "ERROR: 'git reset $_arg' is prohibited in agent sessions. Do not discard commits." >&2
                exit 1
                ;;
            --)
                _has_separator=1
                ;;
            -*)
                ;;
            *)
                _non_flag_args=$((_non_flag_args + 1))
                ;;
        esac
    done

    if [ "$_has_separator" -eq 1 ] && [ "$_non_flag_args" -ge 1 ]; then
        : # allowed — pathspec reset
    elif [ "$_non_flag_args" -ge 2 ]; then
        : # allowed — likely 'git reset <tree-ish> <pathspec>...'
    else
        cat >&2 <<'MSG'
ERROR: 'git reset' without explicit pathspecs is prohibited.

To unstage specific files, use:
  git reset HEAD <path>
  git reset -- <path>
MSG
        exit 1
    fi
fi

# ── Detect integration worktree by cwd ───────────────────────────────────────
# The integration worktree's index and working tree are owned by the
# integration state machine. Its DELIVERY_BOOKKEEPING step commits whatever
# is staged with no path filter, so any mutation introduced from outside the
# state machine can produce a delivery-reverting commit on origin/main.
case "$PWD" in
    */trees/_integration|*/trees/_integration/*)
        _in_integration_wt=1
        ;;
    *)
        _in_integration_wt=0
        ;;
esac

# ── PROTECTED: integrator only ────────────────────────────────────────────────
if [ "$_subcmd" = "stash" ] && [ "$_role" != "integrator" ]; then
    echo "ERROR: 'git stash' is a protected operation. Only the integrator role may use it." >&2
    exit 1
fi

if [ "$_subcmd" = "stash" ] && [ "$_in_integration_wt" = "1" ]; then
    cat >&2 <<'MSG'
ERROR: 'git stash' inside the integration worktree is prohibited.

DELIVERY_BOOKKEEPING commits whatever is staged. Stash/pop can re-stage
content into the next bookkeeping commit.
MSG
    exit 1
fi

if [ "$_subcmd" = "checkout" ]; then
    _has_separator=0
    for _carg in "$@"; do
        [ "$_carg" = "--" ] && _has_separator=1
    done
    if [ "$_has_separator" -eq 0 ]; then
        echo "ERROR: 'git checkout' (branch switch) is a protected operation. Only file-scoped checkout (with --) is allowed." >&2
        exit 1
    fi
    # Inside the integration worktree, file-scoped checkout from a ref is
    # prohibited absolutely — no --confirmed override. The state machine owns
    # the worktree's index and working tree.
    if [ "$_has_separator" -eq 1 ] && [ "$_in_integration_wt" = "1" ]; then
        cat >&2 <<'MSG'
ERROR: 'git checkout <ref> -- <files>' inside the integration worktree is prohibited.

DELIVERY_BOOKKEEPING commits whatever is staged with no path filter.
A checkout here can push a delivery-reverting commit to origin/main.
MSG
        exit 1
    fi
    # During active merge, file-scoped checkout from a ref silently overwrites
    # the local version. Require --confirmed to proceed.
    if [ "$_has_separator" -eq 1 ] && [ -f "$(_real_git ${_dir_args[@]+"${_dir_args[@]}"} rev-parse --git-dir 2>/dev/null)/MERGE_HEAD" ]; then
        _has_co_confirmed=0
        for _carg in "$@"; do
            [ "$_carg" = "--confirmed" ] && _has_co_confirmed=1
        done
        if [ "$_has_co_confirmed" -eq 0 ]; then
            cat >&2 <<'MSG'
WARNING: 'git checkout <ref> -- <files>' during active merge — inspection required.

This overwrites local versions of conflicted files. In a multi-agent
environment, local main often has hotfixes not present on the remote.

Before proceeding:
  1. Run: git diff HEAD -- <file>   (see what local has)
  2. Run: git diff MERGE_HEAD -- <file>   (see what remote has)
  3. Verify the local version has no unique work worth preserving.
  4. Rerun with: git checkout --confirmed <ref> -- <files>
MSG
            exit 1
        fi
        _co_args=()
        for _carg in "$@"; do
            [ "$_carg" = "--confirmed" ] && continue
            _co_args+=("$_carg")
        done
        _real_git "${_co_args[@]}"
        exit $?
    fi
fi

# Block 'git add' for agents — staging is owned by telec.
if [ "$_subcmd" = "add" ]; then
    cat >&2 <<'MSG'
ERROR: 'git add' is not used by agents. Staging is owned by telec.

For NEW files (untracked), stage them explicitly:
  telec code add <path> [<path> ...]
  (This is the sealed equivalent of 'git add' for the only case that needs it.)

For EXISTING modified files, no staging is needed at all. The commit's
pathspec takes them straight from the working tree:
  telec code changed                       # see what's in scope
  telec code commit                        # surface Path A/B/C, then re-run with --approved

NEW files in scope are intentionally NOT auto-committed (catches temp/
scratch artifacts). If you don't `telec code add` them, they stay
untracked. That's by design.

Inspection tools (git diff, git status, git log) remain available.
MSG
    exit 1
fi

if [ "$_subcmd" = "commit" ]; then
    cat >&2 <<'MSG'
ERROR: Use 'telec code commit' instead of 'git commit'. There is no bypass.

telec code commit owns staging, scoping, the pathspec commit, and the
mandated trailer.

Start with no flags:
  telec code commit

That surfaces Path A/B/C. Pick a classification, get human approval if it
requires it, then re-run with --approved and a quoted message. If files
outside the resolved scope must be in the commit, add --include <path>
[<path> ...] at re-run time.
MSG
    exit 1
fi

# Block commit-producing replays on main without explicit approval.
# cherry-pick / rebase / merge produce commits via paths whose outer subcmd
# is not 'commit', so the gate above doesn't see them. Without this, replays
# can land on main bypassing telec code commit's main-branch lock and the
# Path A/B/C approval gate.
case "$_subcmd" in
    cherry-pick|rebase|merge)
        _produces_commit=1
        for _arg in "$@"; do
            case "$_arg" in
                --abort|--skip|--quit|--no-commit)
                    _produces_commit=0
                    ;;
            esac
        done
        if [ "$_produces_commit" -eq 1 ] && [ "${TELECLAUDE_OVERRIDE_APPROVED:-}" != "1" ] && [ "$_role" != "integrator" ]; then
            _current_branch="$(_real_git ${_dir_args[@]+"${_dir_args[@]}"} rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
            if [ "$_current_branch" = "main" ]; then
                cat >&2 <<MSG
ERROR: 'git $_subcmd' on main produces commits without going through the approval gate.

This bypasses telec code commit's main-branch lock, scope checks, and Path A/B/C.

To proceed (human-approved, one-shot):
  TELECLAUDE_OVERRIDE_APPROVED=1 git $_subcmd <args>
MSG
                exit 1
            fi
        fi
        ;;
esac

if [ "$_subcmd" = "push" ]; then
    _current_branch="$(_real_git ${_dir_args[@]+"${_dir_args[@]}"} rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
    if [ "$_current_branch" = "main" ] && [ "$_role" != "integrator" ]; then
        echo "ERROR: Push from main is a protected operation. Only the integrator role may push from main." >&2
        exit 1
    fi
fi

_real_git "$@"
