#!/usr/bin/env bash
#
# capture-evidence.sh  -  produce the visual artefacts the PR and the Jira comment
# cite: the "after" screenshot, and the size-normalised form of anything that has
# to survive an attachment ceiling.
#
# Contract: multi-agent-refs/features/visual-evidence.md
#
# Three jobs, deliberately in one script because they share the naming convention
# that the Jira comment references by filename:
#
#   capture-evidence.sh after  --task <id> --platform <ios|android> --label <slug>
#       Capture the current screen of the running app into the evidence dir.
#
#   capture-evidence.sh video start|stop --task <id> --platform <ios|android> [--label <slug>]
#       Writes <task>[-<label>]-flow.mp4; the label is optional because one
#       recording per task is the common case.
#       Record the screen while something else drives the app (a UI test run, or
#       an MCP-driven flow). start and stop are separate because the recording
#       wraps a run whose duration is not known in advance.
#
#   capture-evidence.sh fit    --file <path> [--max-mb <n>]
#   capture-evidence.sh limits
#       Print the resolved visualEvidence settings as KEY=VALUE lines.
#       Bring one artefact under the attachment ceiling, degrading quality but
#       never dropping the artefact. Prints the path to use (may be a new file).
#
# Env:
#   EVIDENCE_DIR   default "$PWD/.pipeline/evidence"
#   MAX_ATTACH_MB  default from prefs visualEvidence.maxAttachmentMb, else 10
#
# Exit: 0 produced, 2 usage/environment, 4 capture unavailable (no device / no
#       build) - a caller records this as a gap rather than failing the phase.
set -uo pipefail

MODE="${1:-}"; shift 2>/dev/null || true

EVIDENCE_DIR="${EVIDENCE_DIR:-$PWD/.pipeline/evidence}"
PREFS="$HOME/.claude/multi-agent-preferences.json"

# Reads visualEvidence.enabled, visualEvidence.maxAttachmentMb and
# visualEvidence.maxVideoSeconds. One node call: three separate ones cost three
# process starts on every phase boundary.
prefs_visual() {
  node -e '
    const fs=require("fs");
    let v={};
    try{v=((JSON.parse(fs.readFileSync(process.argv[1],"utf8")).global||{}).visualEvidence)||{}}catch{}
    const num=(x,d)=>Number.isFinite(x)&&x>0?x:d;
    process.stdout.write(
      "VISUAL_EVIDENCE_ENABLED="+(v.enabled===false?"false":"true")+"\n"+
      "MAX_ATTACHMENT_MB="+num(v.maxAttachmentMb,10)+"\n"+
      "MAX_VIDEO_SECONDS="+num(v.maxVideoSeconds,60)+"\n");
  ' "$PREFS" 2>/dev/null || printf 'VISUAL_EVIDENCE_ENABLED=true\nMAX_ATTACHMENT_MB=10\nMAX_VIDEO_SECONDS=60\n'
}
eval "$(prefs_visual)"
MAX_ATTACH_MB="${MAX_ATTACH_MB:-$MAX_ATTACHMENT_MB}"

# visualEvidence.enabled off means the whole feature is off: no capture, no
# re-encode, no render. Exit 0 so a phase that calls it unconditionally is not
# failed by a setting the user chose.
if [ "$VISUAL_EVIDENCE_ENABLED" = "false" ] && [ "$MODE" != "limits" ]; then
  echo "capture-evidence: visualEvidence.enabled is false  -  nothing to do" >&2
  exit 0
fi

file_bytes() {
  # Branch on the OS rather than trying one form and falling through: on GNU
  # coreutils `stat -f` is --file-system, so it SUCCEEDS and returns a mount
  # point. The fallback would never run and every size check would compare
  # against a filesystem name.
  case "$(uname -s)" in
    Darwin | *BSD*) stat -f %z "$1" 2>/dev/null || echo 0 ;;
    *) stat -c %s "$1" 2>/dev/null || echo 0 ;;
  esac
}

case "$MODE" in
  after)
    TASK=""; PLATFORM=""; LABEL="screen"
    while [ "$#" -gt 0 ]; do
      case "$1" in
        --task) TASK="${2:-}"; shift 2 ;;
        --platform) PLATFORM="${2:-}"; shift 2 ;;
        --label) LABEL="${2:-screen}"; shift 2 ;;
        *) echo "capture-evidence: unknown option $1" >&2; exit 2 ;;
      esac
    done
    [ -n "$TASK" ] && [ -n "$PLATFORM" ] || {
      echo "usage: capture-evidence.sh after --task <id> --platform <ios|android> [--label <slug>]" >&2
      exit 2
    }
    mkdir -p "$EVIDENCE_DIR"
    OUT="$EVIDENCE_DIR/${TASK}-${LABEL}-after.png"

    # The capture itself goes through the toolkit MCP tools, which the phase doc
    # calls - this script owns the naming, the status-bar hygiene and the
    # downscale, so every caller produces frames that can be compared. When the
    # CLI helpers are absent the caller records a gap; a missing device is not a
    # phase failure.
    case "$PLATFORM" in
      ios)
        command -v xcrun >/dev/null 2>&1 || { echo "capture-evidence: xcrun unavailable" >&2; exit 4; }
        xcrun simctl status_bar booted override \
          --time "9:41" --batteryState charged --batteryLevel 100 \
          --cellularMode active --cellularBars 4 --wifiMode active --wifiBars 3 >/dev/null 2>&1 || true
        xcrun simctl io booted screenshot "$OUT" >/dev/null 2>&1 || { echo "capture-evidence: no booted simulator" >&2; exit 4; }
        ;;
      android)
        command -v adb >/dev/null 2>&1 || { echo "capture-evidence: adb unavailable" >&2; exit 4; }
        adb shell cmd statusbar overlay-icon-visibility hide >/dev/null 2>&1 || true
        adb exec-out screencap -p > "$OUT" 2>/dev/null || { echo "capture-evidence: no attached device" >&2; exit 4; }
        [ -s "$OUT" ] || { echo "capture-evidence: empty capture" >&2; exit 4; }
        ;;
      *) echo "capture-evidence: unsupported platform '$PLATFORM'" >&2; exit 2 ;;
    esac

    # Downscale to 1242px wide when wider. sips ships with macOS; ImageMagick is
    # the fallback. Neither present -> keep the native frame, which is correct,
    # only larger.
    if command -v sips >/dev/null 2>&1; then
      W=$(sips -g pixelWidth "$OUT" 2>/dev/null | awk '/pixelWidth/{print $2}')
      [ -n "$W" ] && [ "$W" -gt 1242 ] 2>/dev/null && sips -Z 1242 "$OUT" >/dev/null 2>&1
    elif command -v magick >/dev/null 2>&1; then
      magick "$OUT" -resize '1242x>' "$OUT" >/dev/null 2>&1
    fi
    printf '%s\n' "$OUT"
    ;;

  video)
    # The flow recording. Shell, not MCP, for the same three reasons the "after"
    # capture is: the screenshot path already shells out, a run whose host has no
    # toolkit MCP registered still produces evidence, and Phase 3 / Phase 5 are
    # exactly where an MCP call is contested. start and stop are separate because
    # the recording wraps a test run whose duration is not known in advance - a
    # fixed --duration either truncates the test or trails dead screen after it.
    ACTION="${1:-}"
    shift 2>/dev/null || true
    # LABEL defaults to EMPTY, not to "flow": the suffix is already "-flow", so a
    # default of "flow" produced <task>-flow-flow.mp4 while every renderer cites
    # <task>-flow.mp4. One recording per task is the common case and it should not
    # have to name itself twice to get the filename the contract quotes.
    TASK=""; PLATFORM=""; LABEL=""
    while [ "$#" -gt 0 ]; do
      case "$1" in
        --task) TASK="${2:-}"; shift 2 ;;
        --platform) PLATFORM="${2:-}"; shift 2 ;;
        --label) LABEL="${2:-}"; shift 2 ;;
        *) echo "capture-evidence: unknown option $1" >&2; exit 2 ;;
      esac
    done
    case "$ACTION" in start | stop) ;; *)
      echo "usage: capture-evidence.sh video start|stop --task <id> --platform <ios|android> [--label <slug>]" >&2
      exit 2 ;;
    esac
    [ -n "$TASK" ] && [ -n "$PLATFORM" ] || {
      echo "usage: capture-evidence.sh video start|stop --task <id> --platform <ios|android> [--label <slug>]" >&2
      exit 2
    }
    case "$PLATFORM" in ios | android) ;; *)
      echo "capture-evidence: unsupported platform '$PLATFORM'" >&2; exit 2 ;;
    esac

    mkdir -p "$EVIDENCE_DIR"
    SLUG="${TASK}${LABEL:+-$LABEL}"
    OUT="$EVIDENCE_DIR/${SLUG}-flow.mp4"
    PIDFILE="$EVIDENCE_DIR/.${SLUG}.recpid"
    WATCHFILE="$EVIDENCE_DIR/.${SLUG}.watchpid"
    REMOTE="/sdcard/_ma_${SLUG}.mp4"

    # screenrecord's own ceiling is 180s and it is not negotiable, so a
    # maxVideoSeconds above it would silently become 180 on Android and stay
    # honoured on iOS - two platforms disagreeing about one preference. Clamp in
    # one place and say so.
    CAP="$MAX_VIDEO_SECONDS"
    if [ "$CAP" -gt 180 ] 2>/dev/null; then
      echo "capture-evidence: maxVideoSeconds $CAP clamped to 180 (screenrecord ceiling)" >&2
      CAP=180
    fi

    if [ "$ACTION" = "start" ]; then
      [ -f "$PIDFILE" ] && { echo "capture-evidence: a recording for $SLUG is already running" >&2; exit 2; }
      rm -f "$OUT"
      case "$PLATFORM" in
        ios)
          command -v xcrun >/dev/null 2>&1 || { echo "capture-evidence: xcrun unavailable" >&2; exit 4; }
          xcrun simctl list devices booted 2>/dev/null | grep -q "(Booted)" \
            || { echo "capture-evidence: no booted simulator" >&2; exit 4; }
          # h264 rather than the hevc default: an hevc mp4 does not play in the
          # Jira attachment preview or in several browsers, which turns the
          # artefact into a download nobody opens.
          xcrun simctl io booted recordVideo --codec h264 --force "$OUT" >/dev/null 2>&1 &
          REC_PID=$!
          ;;
        android)
          command -v adb >/dev/null 2>&1 || { echo "capture-evidence: adb unavailable" >&2; exit 4; }
          adb shell true >/dev/null 2>&1 || { echo "capture-evidence: no attached device" >&2; exit 4; }
          adb shell rm -f "$REMOTE" >/dev/null 2>&1 || true
          adb shell screenrecord --time-limit "$CAP" "$REMOTE" >/dev/null 2>&1 &
          REC_PID=$!
          ;;
      esac

      # A recorder that dies on the first frame leaves a pid file and an empty
      # path, and the caller then "stops" a recording that never ran. Give it a
      # beat and check it actually started.
      sleep 1
      kill -0 "$REC_PID" 2>/dev/null || {
        echo "capture-evidence: recorder exited immediately" >&2
        exit 4
      }
      # On Android the local process is the adb CLIENT, which stays alive even
      # when screenrecord failed on the device, so liveness there proves only
      # that adb is running. The remote file existing is what proves a recording
      # began.
      if [ "$PLATFORM" = "android" ]; then
        adb shell "[ -e '$REMOTE' ]" >/dev/null 2>&1 || {
          kill "$REC_PID" 2>/dev/null || true
          echo "capture-evidence: screenrecord did not start on the device" >&2
          exit 4
        }
      fi
      printf '%s\n' "$REC_PID" > "$PIDFILE"

      # iOS has no --time-limit, so the cap is ours to enforce. Without this a
      # hung UI test records until the disk complains.
      if [ "$PLATFORM" = "ios" ]; then
        (sleep "$CAP"; kill -INT "$REC_PID" 2>/dev/null || true) >/dev/null 2>&1 &
        printf '%s\n' "$!" > "$WATCHFILE"
      fi
      printf '%s\n' "$OUT"
      exit 0
    fi

    # stop
    [ -f "$PIDFILE" ] || { echo "capture-evidence: no recording in progress for $SLUG" >&2; exit 2; }
    REC_PID=$(cat "$PIDFILE" 2>/dev/null)
    rm -f "$PIDFILE"
    if [ -f "$WATCHFILE" ]; then
      kill "$(cat "$WATCHFILE" 2>/dev/null)" 2>/dev/null || true
      rm -f "$WATCHFILE"
    fi

    case "$PLATFORM" in
      ios)
        # SIGINT, not SIGTERM: simctl only writes the moov atom and closes the
        # container on INT. A TERM leaves an mp4 that every player refuses.
        kill -INT "$REC_PID" 2>/dev/null || true
        i=0
        while kill -0 "$REC_PID" 2>/dev/null && [ "$i" -lt 100 ]; do sleep 0.1; i=$((i + 1)); done
        kill -0 "$REC_PID" 2>/dev/null && kill -TERM "$REC_PID" 2>/dev/null || true
        ;;
      android)
        # Match on the output path, not on the program name: a bare
        # `pkill screenrecord` stops every recording on the device, including a
        # second pipeline run's and anything the user started by hand. Fall back
        # to the blunt form only when -f is unavailable.
        adb shell pkill -2 -f "$REMOTE" >/dev/null 2>&1 \
          || adb shell pkill -2 screenrecord >/dev/null 2>&1 || true
        # screenrecord finalises the container after the signal; pulling straight
        # away yields a truncated file that looks like a successful capture.
        sleep 2
        adb pull "$REMOTE" "$OUT" >/dev/null 2>&1 || true
        adb shell rm -f "$REMOTE" >/dev/null 2>&1 || true
        ;;
    esac

    [ -s "$OUT" ] || { echo "capture-evidence: recording produced no file" >&2; exit 4; }

    # Both recorders encode on change, so a flow over a screen that never moved
    # produces a valid two-frame mp4 a fraction of a second long. The file is not
    # broken and must not be discarded, but it is not evidence of a flow either,
    # and the duration is the only thing that can tell the two apart. Say so and
    # let the caller record it; never assert this duration against wall clock,
    # which is what makes a correct static capture look like a failure.
    if command -v ffprobe >/dev/null 2>&1; then
      SECS=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUT" 2>/dev/null)
      case "$SECS" in
        "" ) echo "capture-evidence: ffprobe could not read the recording; duration unknown" >&2 ;;
        * ) awk -v d="$SECS" 'BEGIN { exit (d < 1.0) ? 0 : 1 }' \
              && echo "capture-evidence: recording is ${SECS}s  -  the screen did not change during it" >&2 ;;
      esac
    fi
    printf '%s\n' "$OUT"
    ;;

  fit)
    FILE=""; MAX_MB="$MAX_ATTACH_MB"
    while [ "$#" -gt 0 ]; do
      case "$1" in
        --file) FILE="${2:-}"; shift 2 ;;
        --max-mb) MAX_MB="${2:-$MAX_ATTACH_MB}"; shift 2 ;;
        *) echo "capture-evidence: unknown option $1" >&2; exit 2 ;;
      esac
    done
    [ -n "$FILE" ] && [ -f "$FILE" ] || { echo "usage: capture-evidence.sh fit --file <path> [--max-mb <n>]" >&2; exit 2; }
    LIMIT=$((MAX_MB * 1024 * 1024))
    SIZE=$(file_bytes "$FILE")
    if [ "$SIZE" -le "$LIMIT" ]; then printf '%s\n' "$FILE"; exit 0; fi

    BASE="${FILE%.*}"; EXT="${FILE##*.}"
    case "$EXT" in
      mp4|mov|m4v)
        # Quality down, artefact kept. 720p + a lower bitrate clears an order of
        # magnitude; a flow video is watched for what moves, not for its pixels.
        if command -v ffmpeg >/dev/null 2>&1; then
          SMALL="${BASE}-720p.mp4"
          ffmpeg -y -i "$FILE" -vf "scale='min(1280,iw)':-2" -c:v libx264 -crf 30 \
                 -preset veryfast -an "$SMALL" >/dev/null 2>&1
          if [ -f "$SMALL" ] && [ "$(file_bytes "$SMALL")" -le "$LIMIT" ]; then
            printf '%s\n' "$SMALL"; exit 0
          fi
          # Last resort: four frames as one sheet, so the reviewer still sees the
          # flow even though the recording could not travel.
          SHEET="${BASE}-contact.png"
          DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$FILE" 2>/dev/null | cut -d. -f1)
          [ -n "$DUR" ] && [ "$DUR" -gt 0 ] 2>/dev/null || DUR=4
          ffmpeg -y -i "$FILE" -vf "fps=4/$DUR,scale=480:-2,tile=2x2" -frames:v 1 "$SHEET" >/dev/null 2>&1
          if [ -f "$SHEET" ]; then printf '%s\n' "$SHEET"; exit 0; fi
        fi
        echo "capture-evidence: $FILE exceeds ${MAX_MB}MB and ffmpeg is unavailable" >&2
        printf '%s\n' "$FILE"; exit 0
        ;;
      png|jpg|jpeg)
        SMALL="${BASE}.jpg"
        if command -v sips >/dev/null 2>&1; then
          sips -s format jpeg -s formatOptions 80 "$FILE" --out "$SMALL" >/dev/null 2>&1
        elif command -v magick >/dev/null 2>&1; then
          magick "$FILE" -quality 80 "$SMALL" >/dev/null 2>&1
        fi
        if [ -f "$SMALL" ] && [ "$(file_bytes "$SMALL")" -le "$LIMIT" ]; then
          printf '%s\n' "$SMALL"; exit 0
        fi
        printf '%s\n' "$FILE"; exit 0
        ;;
      *) printf '%s\n' "$FILE"; exit 0 ;;
    esac
    ;;

  limits)
    # The resolved settings as KEY=VALUE, so visualEvidence.maxVideoSeconds is one
    # value read in one place rather than a number repeated across documents. The
    # `video` mode above reads the same function, so the cap a caller prints and
    # the cap the recorder enforces cannot drift apart.
    prefs_visual
    ;;

  *)
    echo "usage: capture-evidence.sh after|video|fit|limits ..." >&2
    exit 2
    ;;
esac
