#!/usr/bin/env bash
# agent-release.sh — 释放 slot
#
# 分级策略（按 workspace + git 状态决定动作）：
#   clean    : 杀心跳 + 删 lock + reset worktree 到 park 分支（回 free 池）
#   dirty    : 杀心跳 + lock 改写为 state=abandoned，保留 worktree 内容
#   unpushed : 同 dirty，reason=unpushed
#
# abandoned slot 不被 claim 自动分配，需要人工处理；用 --force 显式强删。
# 旧接口 --keep-changes（仅释放锁不重置）保留：等价于把 slot 强制改成 abandoned/manual。
set -euo pipefail

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

usage() {
  cat <<EOF >&2
用法:
  bash $0 [slot-num]              # 显式指定 slot；按 workspace 状态自动分级
  bash $0                          # 用 \$FFOA_AGENT_SLOT
  bash $0 --all-stale              # 清掉所有 stale 锁
  bash $0 <slot> --force           # 强删（含 abandoned 与未提交内容）—— 人工确认丢弃后用
  bash $0 <slot> --keep-changes    # 兼容旧接口：仅释放锁不重置（等价 abandoned/manual）
EOF
}

KEEP_CHANGES=false
ALL_STALE=false
FORCE=false
SLOT=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --all-stale) ALL_STALE=true; shift ;;
    --keep-changes) KEEP_CHANGES=true; shift ;;
    --force) FORCE=true; shift ;;
    -h|--help) usage; exit 0 ;;
    *) SLOT="$1"; shift ;;
  esac
done

if [[ "${ALL_STALE}" == true ]]; then
  for s in $(ap_list_slots); do
    if [[ "$(ap_slot_state "${s}")" == stale ]]; then
      echo "agent-release: 处理 stale slot-${s}"
      bash "$0" "${s}"
    fi
  done
  exit 0
fi

if [[ -z "${SLOT}" ]]; then
  SLOT="${FFOA_AGENT_SLOT:-}"
fi

if [[ -z "${SLOT}" ]]; then
  echo "agent-release: 未指定 slot 编号，且 \$FFOA_AGENT_SLOT 为空" >&2
  usage
  exit 1
fi

LOCK_FILE="$(ap_lock_file "${SLOT}")"
SLOT_DIR="$(ap_slot_dir "${SLOT}")"

if [[ ! -f "${LOCK_FILE}" ]]; then
  echo "agent-release: slot-${SLOT} 当前未占用，无需释放"
  exit 0
fi

CURRENT_STATE="$(ap_read_lock "${SLOT}" state || echo '')"
HEARTBEAT_PID="$(ap_read_lock "${SLOT}" heartbeat_pid || true)"
HOST="$(ap_read_lock "${SLOT}" host || true)"
TASK_BRANCH="$(ap_read_lock "${SLOT}" task_branch || true)"

# 判定动作：force / keep-changes 走旧路径；否则按 workspace 状态分级
ws_status="clean"
if [[ "${FORCE}" != true && "${KEEP_CHANGES}" != true ]]; then
  ws_status="$(ap_workspace_status "${SLOT}")"
fi

# 杀心跳守护（任何分支都要杀，避免长生不老）
# 例外：release 由 heartbeat 自检触发时（$PPID == HEARTBEAT_PID），跳过 kill。
# 否则形成"父杀子等子返回"的自杀链——能 work 是因为 release 进程会被 reparent 到 init，
# 但隐晦且 release 之前 fail 时 heartbeat 会卡住。让 heartbeat 自己 break 退出更干净。
if [[ -n "${HEARTBEAT_PID}" && "${HOST}" == "$(hostname)" && "${PPID}" != "${HEARTBEAT_PID}" ]]; then
  if kill -0 "${HEARTBEAT_PID}" 2>/dev/null; then
    kill "${HEARTBEAT_PID}" 2>/dev/null || true
  fi
fi

if [[ "${KEEP_CHANGES}" == true ]]; then
  ap_mark_abandoned "${SLOT}" "manual"
  ap_write_code_workspace || true
  cat >&2 <<EOF
agent-release: slot-${SLOT} 标记为 abandoned/manual（分支: ${TASK_BRANCH}，worktree 保留）
  ⚠ BREAKING：--keep-changes 行为已变更
    旧版：删 lock + 保留 worktree → slot 立刻回 free 池，可被新 claim 占用
    新版：标 state=abandoned，lock + worktree 都保留 → slot 不被 claim/sweep 自动碰
  完成接手：commit/push 后跑 bash $0 ${SLOT} --keep-changes 再次调用 OR 丢弃用 --force
EOF
  exit 0
fi

if [[ "${FORCE}" != true && "${ws_status}" != "clean" ]]; then
  ap_mark_abandoned "${SLOT}" "${ws_status}"
  ap_write_code_workspace || true
  cat >&2 <<EOF
agent-release: slot-${SLOT} 标记为 abandoned/${ws_status}（分支: ${TASK_BRANCH}）
  worktree 保留：${SLOT_DIR}
  人工处理：
    - 继续：bash $0 ${SLOT} --keep-changes  # 改前先 cd 进去 commit/push
    - 丢弃：bash $0 ${SLOT} --force         # 强 reset，未提交内容会进 wip/ 分支兜底
EOF
  exit 0
fi

# 真释放：删 lock + reset worktree + 清掉无价值的本地 task 分支 ref
rm -f "${LOCK_FILE}"

if [[ -d "${SLOT_DIR}" ]]; then
  PARK_BRANCH="pool/slot-${SLOT}"
  (
    cd "${SLOT_DIR}"

    # 未提交改动 stash 到 wip 分支保命（force 路径才会走到这里）
    if ! git diff --quiet HEAD -- 2>/dev/null || \
       [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then
      WIP_BRANCH="wip/slot-${SLOT}-$(date +%Y%m%d-%H%M%S)"
      echo "agent-release: 检测到未提交改动，保存到 ${WIP_BRANCH}"
      git add -A 2>/dev/null || true
      git commit -m "wip: slot-${SLOT} release auto-stash" --no-verify >/dev/null 2>&1 || true
      git branch "${WIP_BRANCH}" 2>/dev/null || true
    fi

    # 切回 park 分支（无则新建）—— 必须先切走才能删 task 分支 ref
    if ! git show-ref --verify --quiet "refs/heads/${PARK_BRANCH}"; then
      git checkout -B "${PARK_BRANCH}" origin/develop >/dev/null 2>&1 || \
        git checkout -B "${PARK_BRANCH}" >/dev/null 2>&1
    else
      git checkout "${PARK_BRANCH}" >/dev/null 2>&1
    fi

    git reset --hard "${PARK_BRANCH}" >/dev/null 2>&1 || true
    git clean -fdx -e node_modules -e .next -e .env -e '.env.*' -e TASK.md >/dev/null 2>&1 || true

    # 删本地 task 分支 ref：仅当（a）已被任一 protected base 吸收，或（b）远端已删。
    # 任一条件成立 → 本地 ref 没保留价值，留着会让下次同名 claim 拿到 stale HEAD。
    # 不满足 → 保留本地 ref，避免误删未上游化的 work。
    #
    # "远端已删"用本地 refs/remotes/origin/<task-branch> cache 判，不走 ls-remote 网络
    # 调用。前提：CLAUDE.md 铁律 fetch.prune=true，远端分支删了 next fetch 会清掉本地
    # cache。这样离线场景也安全（最坏：保留本地 ref，下次 claim 自动 ff 同步）。
    # 顺序：absorbed 检查在前，offline 友好；ref 不在再看远端。
    if [[ -n "${TASK_BRANCH}" && "${TASK_BRANCH}" != pool/slot-* ]] \
       && git show-ref --verify --quiet "refs/heads/${TASK_BRANCH}"; then
      delete_reason=""
      # 把 absorbed 检测的 stderr 写到 sweep log 而不是 /dev/null，方便事后排查
      # （patch-id binary 缺失 / git diff fail 等会无声降级到 absorbed=NO，导致本地
      # ref 被错误保留）。
      ABSORB_LOG="$(ap_pool_root)/sweep.log"
      if ap_ref_absorbed "${SLOT_DIR}" "refs/heads/${TASK_BRANCH}" 2>>"${ABSORB_LOG}"; then
        delete_reason="已被 protected base 吸收"
      elif ! git show-ref --verify --quiet "refs/remotes/origin/${TASK_BRANCH}"; then
        delete_reason="远端已删"
      fi

      if [[ -n "${delete_reason}" ]]; then
        if git branch -D "${TASK_BRANCH}" >/dev/null 2>&1; then
          echo "agent-release: 删除本地分支 ${TASK_BRANCH}（${delete_reason}）"
        fi
      else
        echo "agent-release: 保留本地分支 ${TASK_BRANCH}（远端在 + 未吸收）"
      fi
    fi
  ) >&2
fi

ap_write_code_workspace || true

echo "agent-release: slot-${SLOT} 已释放（原分支: ${TASK_BRANCH}）"
