#!/usr/bin/env bash
# audit-log-rotate.sh  -  daily rotation for ~/.claude/logs/multi-agent/audit.jsonl.
#
# Per section18.2.4 of REFACTOR_PLAN_v3.7.md: audit log rotates daily, keeps 30 days
# of compressed history, drops anything older.
#
# Install via launchd on macOS (preferred) or cron on Linux:
#
#   macOS launchd plist (~/Library/LaunchAgents/com.claude.audit-rotate.plist):
#     <plist version="1.0"><dict>
#       <key>Label</key><string>com.claude.audit-rotate</string>
#       <key>ProgramArguments</key>
#       <array>
#         <string>/bin/bash</string>
#         <string>$HOME/.claude/scripts/audit-log-rotate.sh</string>
#       </array>
#       <key>StartCalendarInterval</key>
#       <dict><key>Hour</key><integer>3</integer><key>Minute</key><integer>0</integer></dict>
#     </dict></plist>
#   Then: launchctl load ~/Library/LaunchAgents/com.claude.audit-rotate.plist
#
#   Linux cron (crontab -e):
#     0 3 * * * bash $HOME/.claude/scripts/audit-log-rotate.sh
#
# Run manually anytime: bash audit-log-rotate.sh

set -uo pipefail

AUDIT_DIR="${AUDIT_DIR:-$HOME/.claude/logs/multi-agent}"
AUDIT_FILE="$AUDIT_DIR/audit.jsonl"
KEEP_DAYS="${AUDIT_KEEP_DAYS:-30}"

mkdir -p "$AUDIT_DIR"

# Rotate today's file (only if it has content)
if [ -s "$AUDIT_FILE" ]; then
  # Portable file-mtime read. BSD `date -r` takes epoch SECONDS (not a file
  # path), so the previous `date -r "$AUDIT_FILE"` always errored on macOS and
  # mislabeled every rotated file with today's date.
  case "$(uname -s)" in
    Darwin|*BSD*) MTIME="$(stat -f %m "$AUDIT_FILE" 2>/dev/null || true)" ;;
    *)            MTIME="$(stat -c %Y "$AUDIT_FILE" 2>/dev/null || true)" ;;
  esac
  if [ -n "$MTIME" ]; then
    # BSD: date -r <epoch>; GNU: date -d @<epoch>
    STAMP="$(date -u -r "$MTIME" +"%Y-%m-%d" 2>/dev/null \
      || date -u -d "@$MTIME" +"%Y-%m-%d" 2>/dev/null \
      || date -u +"%Y-%m-%d")"
  else
    STAMP="$(date -u +"%Y-%m-%d")"
  fi
  ROTATED="$AUDIT_DIR/audit.${STAMP}.jsonl"

  # Atomic against concurrent appenders: mv the live inode FIRST, then create
  # a fresh empty live file. Writers appending via >> to the moved inode land
  # in the spool and are preserved; the old cat-then-truncate flow lost any
  # event appended between the copy and the truncate.
  SPOOL="$AUDIT_DIR/.audit.rotate.$$"
  mv "$AUDIT_FILE" "$SPOOL"
  # touch, not truncate: a writer appending between the mv and this line
  # recreates the live file, and `: >` would destroy that event. touch keeps
  # whatever a racing writer already appended.
  touch "$AUDIT_FILE"
  chmod 600 "$AUDIT_FILE" 2>/dev/null || true

  # Don't clobber a rotated file from earlier today (multiple invocations).
  # Earlier rotations may already be gzipped; appending a second gzip member
  # is valid (gunzip concatenates members).
  if [ -f "$ROTATED.gz" ] && command -v gzip >/dev/null 2>&1; then
    if gzip -c "$SPOOL" >> "$ROTATED.gz" 2>/dev/null; then
      rm -f "$SPOOL"
    else
      mv "$SPOOL" "$ROTATED"
    fi
  elif [ -f "$ROTATED" ]; then
    cat "$SPOOL" >> "$ROTATED"
    rm -f "$SPOOL"
  else
    mv "$SPOOL" "$ROTATED"
  fi

  # gzip immediately to save space
  if [ -f "$ROTATED" ] && command -v gzip >/dev/null 2>&1; then
    gzip -f "$ROTATED" 2>/dev/null || true
  fi

  # gzip preserves the source file's mtime by default, so an archive rotated
  # from log content already older than KEEP_DAYS would carry that same old
  # mtime - and the retention sweep below deletes anything past KEEP_DAYS,
  # so a just-created archive could be purged in this very run before ever
  # being read. Retention has to count from rotation time, not from the age
  # of the content it archived. gzip -f removes the plain file on success, so
  # only touch whichever of the two actually exists.
  [ -f "$ROTATED.gz" ] && touch "$ROTATED.gz"
  [ -f "$ROTATED" ] && touch "$ROTATED"
fi

# Drop archives older than KEEP_DAYS
find "$AUDIT_DIR" -maxdepth 1 -name "audit.*.jsonl*" -type f \
  -mtime "+$KEEP_DAYS" -delete 2>/dev/null || true

echo "audit rotated: kept ${KEEP_DAYS}d of history at $AUDIT_DIR"
