#!/usr/bin/env bash
set -euo pipefail

branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
if [[ -z "$branch" || "$branch" == "HEAD" ]]; then
  branch="$(git symbolic-ref --quiet --short HEAD 2>/dev/null || true)"
fi
if [[ -z "$branch" ]]; then
  exit 0
fi

repo_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [[ -z "$repo_root" ]]; then
  exit 0
fi
NODE_BIN="${GUARDEX_NODE_BIN:-node}"
CLI_ENTRY="${GUARDEX_CLI_ENTRY:-}"
guardex_env_helper="${repo_root}/scripts/guardex-env.sh"
if [[ -f "$guardex_env_helper" ]]; then
  # shellcheck source=/dev/null
  source "$guardex_env_helper"
fi
if declare -F guardex_repo_is_enabled >/dev/null 2>&1 && ! guardex_repo_is_enabled "$repo_root"; then
  exit 0
fi

run_guardex_cli() {
  if [[ -n "$CLI_ENTRY" ]]; then
    "$NODE_BIN" "$CLI_ENTRY" "$@"
    return $?
  fi
  if command -v gx >/dev/null 2>&1; then
    gx "$@"
    return $?
  fi
  if command -v gitguardex >/dev/null 2>&1; then
    gitguardex "$@"
    return $?
  fi
  echo "[agent-branch-guard] Guardex CLI entrypoint unavailable; rerun via gx." >&2
  return 127
}

if [[ "${ALLOW_COMMIT_ON_PROTECTED_BRANCH:-0}" == "1" ]]; then
  exit 0
fi

is_unborn_branch=0
if ! git rev-parse --verify HEAD >/dev/null 2>&1; then
  is_unborn_branch=1
fi

is_codex_session=0
if [[ -n "${CODEX_THREAD_ID:-}" || -n "${OMX_SESSION_ID:-}" || "${CODEX_CI:-0}" == "1" ]]; then
  is_codex_session=1
fi

# Superset of is_codex_session that also covers Claude Code sessions so the
# protected-branch gate below only triggers for automated agents — humans stay
# free to commit directly on main/dev/master.
is_agent_session=$is_codex_session
if [[ -n "${CLAUDECODE:-}" || -n "${CLAUDE_CODE_SESSION_ID:-}" ]]; then
  is_agent_session=1
fi

is_vscode_git_context=0
if [[ -n "${VSCODE_GIT_IPC_HANDLE:-}" || -n "${VSCODE_GIT_ASKPASS_NODE:-}" || -n "${VSCODE_IPC_HOOK_CLI:-}" ]]; then
  is_vscode_git_context=1
fi

allow_vscode_protected_raw="${GUARDEX_ALLOW_VSCODE_PROTECTED_BRANCH_WRITES:-$(git config --get multiagent.allowVscodeProtectedBranchWrites || true)}"
if [[ -z "$allow_vscode_protected_raw" ]]; then
  allow_vscode_protected_raw="false"
fi
allow_vscode_protected="$(printf '%s' "$allow_vscode_protected_raw" | tr '[:upper:]' '[:lower:]')"

allow_vscode_protected_branch_writes=0
case "$allow_vscode_protected" in
  1|true|yes|on) allow_vscode_protected_branch_writes=1 ;;
  0|false|no|off) allow_vscode_protected_branch_writes=0 ;;
  *) allow_vscode_protected_branch_writes=0 ;;
esac

protected_branches_raw="${GUARDEX_PROTECTED_BRANCHES:-$(git config --get multiagent.protectedBranches || true)}"
if [[ -z "$protected_branches_raw" ]]; then
  protected_branches_raw="dev main master"
fi
protected_branches_raw="${protected_branches_raw//,/ }"

is_protected_branch=0
for protected_branch in $protected_branches_raw; do
  if [[ "$branch" == "$protected_branch" ]]; then
    is_protected_branch=1
    break
  fi
done

codex_require_agent_branch_raw="${GUARDEX_CODEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.codexRequireAgentBranch || true)}"
if [[ -z "$codex_require_agent_branch_raw" ]]; then
  codex_require_agent_branch_raw="true"
fi
codex_require_agent_branch="$(printf '%s' "$codex_require_agent_branch_raw" | tr '[:upper:]' '[:lower:]')"

should_require_codex_agent_branch=0
case "$codex_require_agent_branch" in
  1|true|yes|on) should_require_codex_agent_branch=1 ;;
  0|false|no|off) should_require_codex_agent_branch=0 ;;
  *) should_require_codex_agent_branch=1 ;;
esac

# General lockdown knob (applies to ALL agent sessions, not just Codex).
# Default OFF: any branch that is not a protected base is an acceptable agent
# branch, so `vendor/x`, `feat/y`, or any ad-hoc name commits without ceremony.
# Set GUARDEX_REQUIRE_AGENT_BRANCH=1 (or `git config multiagent.requireAgentBranch
# true`) to force agent commits back onto the agent/* namespace.
require_agent_branch_raw="${GUARDEX_REQUIRE_AGENT_BRANCH:-$(git config --get multiagent.requireAgentBranch || true)}"
require_agent_branch="$(printf '%s' "$require_agent_branch_raw" | tr '[:upper:]' '[:lower:]')"
should_require_agent_branch=0
case "$require_agent_branch" in
  1|true|yes|on) should_require_agent_branch=1 ;;
esac

is_codex_managed_only_commit_on_protected=0
if [[ "$is_codex_session" == "1" && "$is_protected_branch" == "1" ]]; then
  deleted_paths="$(git diff --cached --name-only --diff-filter=D)"
  staged_paths="$(git diff --cached --name-only --diff-filter=ACMRTUXB)"
  if [[ -z "$deleted_paths" && -n "$staged_paths" ]]; then
    managed_only=1
    while IFS= read -r staged_path; do
      case "$staged_path" in
        AGENTS.md|.gitignore) ;;
        *) managed_only=0; break ;;
      esac
    done <<< "$staged_paths"
    if [[ "$managed_only" == "1" ]]; then
      is_codex_managed_only_commit_on_protected=1
    fi
  fi
fi

if [[ "$should_require_codex_agent_branch" == "1" && "${GUARDEX_ALLOW_CODEX_ON_NON_AGENT:-0}" != "1" ]]; then
  if [[ "$is_codex_session" == "1" && "$branch" != agent/* ]]; then
    if [[ "$is_protected_branch" == "1" ]]; then
      if [[ "$is_codex_managed_only_commit_on_protected" == "1" ]]; then
        exit 0
      fi

      cat >&2 <<'MSG'
[guardex-preedit-guard] Codex edit/commit detected on a protected branch.
GitGuardex requires Codex work to run from an isolated agent/* branch.
Start the sub-branch/worktree with:
  gx branch start "<task-or-plan>" "<agent-name>"
Then commit from the created agent/* branch.

Temporary bypass (not recommended):
  GUARDEX_ALLOW_CODEX_ON_NON_AGENT=1 git commit ...
MSG
      exit 1
    fi
    # Non-protected branches (vendor/, feat/, any ad-hoc name) are fine for
    # Codex too — being OFF a protected base is the only load-bearing rule.
    # Re-impose the agent/* requirement with GUARDEX_REQUIRE_AGENT_BRANCH=1
    # (handled by the general lockdown gate below).
  fi
fi

if [[ "$is_protected_branch" == "1" ]]; then
  # Humans may commit directly on protected branches; only agent sessions
  # (Codex / Claude Code / OMX) are blocked.
  if [[ "$is_agent_session" != "1" ]]; then
    exit 0
  fi

  if [[ "$is_unborn_branch" == "1" && "$is_codex_session" != "1" ]]; then
    exit 0
  fi

  git_dir="$(git rev-parse --git-dir)"
  if [[ -f "$git_dir/MERGE_HEAD" ]]; then
    exit 0
  fi

  cat >&2 <<'MSG'
[agent-branch-guard] Direct commits on protected branches are blocked.
Use an agent branch first:
  gx branch start "<task-or-plan>" "<agent-name>"
After finishing work:
  gx branch finish

Temporary bypass (not recommended):
  ALLOW_COMMIT_ON_PROTECTED_BRANCH=1 git commit ...
MSG
  exit 1
fi

if [[ "$is_agent_session" == "1" && "$branch" != agent/* && "$should_require_agent_branch" == "1" ]]; then
  cat >&2 <<'MSG'
[agent-branch-guard] Lockdown mode: agent commits must run on dedicated agent/* branches.
GUARDEX_REQUIRE_AGENT_BRANCH (or multiagent.requireAgentBranch) is enabled.
Start an agent branch first:
  gx branch start "<task-or-plan>" "<agent-name>"
Then commit on that branch.

Relax (any non-protected branch is normally fine):
  unset GUARDEX_REQUIRE_AGENT_BRANCH
  # or: git config multiagent.requireAgentBranch false
MSG
  exit 1
fi

if [[ "$branch" == agent/* ]]; then
  if [[ "${GUARDEX_AUTOCLAIM_STAGED_LOCKS:-1}" == "1" ]]; then
    # Auto-claim non-deletion staged paths. Deletions need an explicit
    # `--allow-delete` flag below so `locks validate --staged` doesn't
    # reject the commit on the same trip the user staged the delete.
    while IFS= read -r staged_file; do
      [[ -z "$staged_file" ]] && continue
      [[ "$staged_file" == ".omx/state/agent-file-locks.json" ]] && continue
      run_guardex_cli locks claim --branch "$branch" "$staged_file" >/dev/null 2>&1 || true
    done < <(git diff --cached --name-only --diff-filter=ACMRTUXB)

    # Auto-approve deletions for the same branch (gated separately so
    # operators can disable this single behavior without disabling the
    # broader auto-claim). Defaults to enabled — matches the auto-claim
    # default and removes the "first commit fails, then `gx locks
    # allow-delete`, then commit again" loop.
    if [[ "${GUARDEX_AUTOCLAIM_STAGED_DELETES:-1}" == "1" ]]; then
      _staged_deletes=()
      while IFS= read -r staged_delete; do
        [[ -z "$staged_delete" ]] && continue
        [[ "$staged_delete" == ".omx/state/agent-file-locks.json" ]] && continue
        _staged_deletes+=("$staged_delete")
      done < <(git diff --cached --name-only --diff-filter=D)
      if (( ${#_staged_deletes[@]} > 0 )); then
        run_guardex_cli locks claim --branch "$branch" --allow-delete \
          "${_staged_deletes[@]}" >/dev/null 2>&1 || true
      fi
    fi
  fi

  if ! run_guardex_cli locks validate --branch "$branch" --staged; then
    cat >&2 <<'MSG'
[agent-branch-guard] Agent branch commits require file ownership locks.
Claim files first:
  gx locks claim --branch "$(git rev-parse --abbrev-ref HEAD)" <file...>
MSG
    exit 1
  fi

  require_sync_before_commit_raw="$(git config --get multiagent.sync.requireBeforeCommit || true)"
  if [[ -z "$require_sync_before_commit_raw" ]]; then
    require_sync_before_commit_raw="false"
  fi
  require_sync_before_commit="$(printf '%s' "$require_sync_before_commit_raw" | tr '[:upper:]' '[:lower:]')"

  should_require_sync=0
  case "$require_sync_before_commit" in
    1|true|yes|on) should_require_sync=1 ;;
    0|false|no|off) should_require_sync=0 ;;
    *) should_require_sync=0 ;;
  esac

  if [[ "$should_require_sync" == "1" ]]; then
    base_branch="$(git config --get multiagent.baseBranch || true)"
    if [[ -z "$base_branch" ]]; then
      base_branch="dev"
    fi

    max_behind_raw="$(git config --get multiagent.sync.maxBehindCommits || true)"
    if [[ -z "$max_behind_raw" ]]; then
      max_behind_raw="0"
    fi
    if [[ ! "$max_behind_raw" =~ ^[0-9]+$ ]]; then
      echo "[agent-sync-guard] Invalid multiagent.sync.maxBehindCommits value: ${max_behind_raw}" >&2
      echo "[agent-sync-guard] Expected non-negative integer. Example: git config multiagent.sync.maxBehindCommits 0" >&2
      exit 1
    fi

    if ! git fetch origin "$base_branch" --quiet >/dev/null 2>&1; then
      echo "[agent-sync-guard] Unable to fetch origin/${base_branch} while commit sync gate is enabled." >&2
      echo "[agent-sync-guard] Disable gate temporarily with: git config multiagent.sync.requireBeforeCommit false" >&2
      exit 1
    fi

    if ! git show-ref --verify --quiet "refs/remotes/origin/${base_branch}"; then
      echo "[agent-sync-guard] Remote base branch not found: origin/${base_branch}" >&2
      exit 1
    fi

    behind_count="$(git rev-list --left-right --count "${branch}...origin/${base_branch}" 2>/dev/null | awk '{print $2}')"
    behind_count="${behind_count:-0}"
    max_behind="${max_behind_raw}"

    if [[ "$behind_count" -gt "$max_behind" ]]; then
      cat >&2 <<MSG
[agent-sync-guard] Commit blocked: '${branch}' is behind origin/${base_branch} by ${behind_count} commit(s) (max allowed: ${max_behind}).
Run:
  gx sync --base ${base_branch}
Or relax threshold:
  git config multiagent.sync.maxBehindCommits <n>
MSG
      exit 1
    fi
  fi
fi

if command -v pre-commit >/dev/null 2>&1 && [[ -f .pre-commit-config.yaml ]]; then
  pre-commit run --hook-stage pre-commit
fi
