#!/usr/bin/env bash
#
# repo-hygiene.sh
# Keeps pipeline artefacts out of the user's repository. Sourceable library.
#
# The pipeline writes into a checkout in three ways: the worktree parent
# (.worktrees/), the per-run artefact dir (.pipeline/), and the offload tree
# (.multi-agent/). Only the first was ever excluded, and only by a one-line
# guard duplicated across two phase docs and gc-worktrees.sh. The other two
# were never excluded anywhere, which is invisible in worktree mode (the
# worktree is deleted) and permanent in --local mode (nothing deletes it).
#
# Functions:
#   ma_hygiene_ensure_exclusions <repo-root>   Write the managed block into
#                                              .git/info/exclude. Idempotent;
#                                              rewrites the block on upgrade so
#                                              new entries reach old checkouts.
#   ma_hygiene_release_exclusions <repo-root>  Remove the managed block, and
#                                              nothing else. Lines a human put
#                                              there are never touched.
#   ma_hygiene_local_gitignore <repo-root>     Write .multi-agent/.gitignore.
#   ma_hygiene_prune_empty <repo-root>         rmdir the artefact parents when
#                                              their last child is gone.
#
# Every function is best-effort: a read-only or non-git directory is not an
# error, because refusing to run a pipeline over a repo we cannot tidy is worse
# than leaving it untidy. Callers do not check the return value.
#
# bash 3.2 compatible (macOS ships 3.2 and has no flock, no mapfile).

MA_HYGIENE_BEGIN='# >>> multi-agent pipeline (managed) >>>'
MA_HYGIENE_END='# <<< multi-agent pipeline (managed) <<<'

# Paths the pipeline writes inside a checkout. `.worktrees/` predates this file
# and may already be present as a bare line; git treats a duplicated pattern as
# one, so the managed block restates it rather than trying to adopt it.
ma_hygiene_patterns() {
  cat <<'PATTERNS'
.worktrees/
.pipeline/
.multi-agent/
triage-output.json
.review-diff.txt
.build.log
.test.log
PATTERNS
}

# Same contract every gc-*.sh in this tree carries: refuse to operate on `/`,
# on $HOME, or on a path that does not resolve. $HOME matters specifically -
# `$HOME/.multi-agent/` is a real directory the installer knows about, so a
# caller that passed the wrong root would have had `prune_empty` delete a file
# out of it. Callers never check the return value; refusing is silent and safe.
#
# One consequence worth knowing: offload-ref.sh and bulk-read.sh fall back to
# $PWD when they are not inside a repo, so offloading from $HOME itself now
# skips writing the ignore file. The payload is still written; only the ignore
# is skipped, and $HOME/.multi-agent/ is a tree the installer prunes anyway.
ma_hygiene_safe_root() {
  local root="${1:-}" resolved
  [ -n "$root" ] || return 1
  resolved=$(cd "$root" 2>/dev/null && pwd -P) || return 1
  [ -n "$resolved" ] || return 1
  [ "$resolved" = "/" ] && return 1
  [ "$resolved" = "$(cd "$HOME" 2>/dev/null && pwd -P)" ] && return 1
  printf '%s\n' "$resolved"
}

# Resolve the exclude file through --git-common-dir, not --git-dir: inside a
# worktree the latter points at .git/worktrees/<name>, whose info/exclude is
# per-worktree and dies with it.
ma_hygiene_exclude_path() {
  local root="$1" common
  common=$(git -C "$root" rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || return 1
  [ -n "$common" ] || return 1
  printf '%s/info/exclude\n' "$common"
}

ma_hygiene_ensure_exclusions() {
  local root ex tmp
  root=$(ma_hygiene_safe_root "${1:-}") || return 0
  ex=$(ma_hygiene_exclude_path "$root") || return 0
  mkdir -p "$(dirname "$ex")" 2>/dev/null || return 0
  [ -f "$ex" ] || : > "$ex" 2>/dev/null || return 0

  tmp="${ex}.ma-tmp.$$"
  # Drop any previous managed block, keep everything else verbatim, then append
  # the current one. sed is the portable way to delete an inclusive range.
  if grep -qF "$MA_HYGIENE_BEGIN" "$ex" 2>/dev/null; then
    sed "/^$(printf '%s' "$MA_HYGIENE_BEGIN" | sed 's/[][\.*^$\/]/\\&/g')$/,/^$(printf '%s' "$MA_HYGIENE_END" | sed 's/[][\.*^$\/]/\\&/g')$/d" "$ex" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
  else
    cat "$ex" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
  fi

  # A file whose last line has no newline would take the BEGIN marker onto the
  # end of it. That corrupts the user's line AND leaves the marker unanchored,
  # so `release_exclusions` can never match it again and the block is stuck in
  # their file for good. Close the line first.
  if [ -s "$tmp" ] && [ "$(tail -c 1 "$tmp" | wc -l | tr -d ' ')" = "0" ]; then
    printf '\n' >> "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
  fi

  {
    printf '%s\n' "$MA_HYGIENE_BEGIN"
    ma_hygiene_patterns
    printf '%s\n' "$MA_HYGIENE_END"
  } >> "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }

  mv "$tmp" "$ex" 2>/dev/null || rm -f "$tmp"
  return 0
}

ma_hygiene_release_exclusions() {
  local root ex tmp
  root=$(ma_hygiene_safe_root "${1:-}") || return 0
  ex=$(ma_hygiene_exclude_path "$root") || return 0
  [ -f "$ex" ] || return 0
  grep -qF "$MA_HYGIENE_BEGIN" "$ex" 2>/dev/null || return 0

  tmp="${ex}.ma-tmp.$$"
  sed "/^$(printf '%s' "$MA_HYGIENE_BEGIN" | sed 's/[][\.*^$\/]/\\&/g')$/,/^$(printf '%s' "$MA_HYGIENE_END" | sed 's/[][\.*^$\/]/\\&/g')$/d" "$ex" > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
  mv "$tmp" "$ex" 2>/dev/null || rm -f "$tmp"
  return 0
}

# `*` and not a list of children: an ignore file that names its siblings does
# not name itself, so .multi-agent/.gitignore stayed permanently untracked -
# the one piece of residue the guard existed to prevent. Git reads an ignore
# file whether or not it is itself ignored, so self-exclusion is safe.
ma_hygiene_local_gitignore() {
  local root gi
  root=$(ma_hygiene_safe_root "${1:-}") || return 0
  gi="$root/.multi-agent/.gitignore"
  mkdir -p "$root/.multi-agent" 2>/dev/null || return 0
  if [ ! -f "$gi" ] || ! grep -q '^\*$' "$gi" 2>/dev/null; then
    printf '# Local run artefacts  -  never commit. Ignores this file too.\n*\n' > "$gi" 2>/dev/null || return 0
  fi
  return 0
}

# rmdir, never rm -rf: it fails harmlessly when anything is left, which is the
# behaviour we want. An empty .worktrees/ and .pipeline/evidence/ survive a
# clean run today and read as leftovers to anyone looking at the checkout.
ma_hygiene_prune_empty() {
  local root d
  root=$(ma_hygiene_safe_root "${1:-}") || return 0
  for d in "$root/.pipeline/evidence" "$root/.pipeline" "$root/.worktrees/.archive" "$root/.worktrees" "$root/.multi-agent/refs"; do
    [ -d "$d" ] && rmdir "$d" 2>/dev/null
  done

  # .multi-agent survives the loop above because it still holds the .gitignore
  # this library wrote. That file is ours, so a directory holding nothing else
  # is residue, not user data - but `memory/` there IS user data and is never
  # touched (see commands/multi-agent/uninstall/SKILL.md).
  if [ -d "$root/.multi-agent" ]; then
    local leftover
    leftover=$(ls -A "$root/.multi-agent" 2>/dev/null)
    if [ "$leftover" = ".gitignore" ]; then
      rm -f "$root/.multi-agent/.gitignore" 2>/dev/null
      rmdir "$root/.multi-agent" 2>/dev/null
    fi
  fi
  return 0
}
