#!/usr/bin/env bash
# aisstack-review-log — atomically log a review result
# Usage: aisstack-review-log '{"skill":"...","timestamp":"...","status":"..."}'
#
# Before reading a diff: aisstack-review-log --start review
# After that pass: aisstack-review-log '{...,"completed":true,"converged":true}' --finish TOKEN
# With a findings file: aisstack-review-log '{...}' --findings findings.jsonl
#   status/unresolved/issues_found/critical_gaps are DERIVED from the file (B7);
#   a claimed value that disagrees is rejected with REVIEW_STATUS_MISMATCH.
# Exit: 0 written · 1 rejected (binding or validation failed; stderr says why) · 2 usage.
# Diff reviews get wtree only from a consumed, matching start capture on an
# unchanged tree. Completion/convergence are reviewer-reported, not proof that
# a model read the code. Caller-supplied binding fields are always discarded.
# Plan-tier rows retain their legacy binding behavior.
set -euo pipefail
export GIT_OPTIONAL_LOCKS=0
export GIT_CONFIG_PARAMETERS="${GIT_CONFIG_PARAMETERS:+$GIT_CONFIG_PARAMETERS }'core.fsmonitor=false' 'core.untrackedCache=false'"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
eval "$("$SCRIPT_DIR/aisstack-slug" 2>/dev/null)"
. "$(dirname "$0")/aisstack-state-root.sh" 2>/dev/null || { echo "$0: cannot resolve the aisstack state root: $(dirname "$0")/aisstack-state-root.sh is missing. fix: reinstall with ./setup or /aisstack-upgrade (docs/state-root.md)" >&2; exit 1; }
aisstack_state_root_select
AISSTACK_HOME="$_aisstack_sr_root"
mkdir -p "$AISSTACK_HOME/projects/$SLUG"

INPUT="${1:-}"
export AISSTACK_REVIEW_DIR="$AISSTACK_HOME/projects/$SLUG"
export AISSTACK_REVIEW_REPO="$(git rev-parse --show-toplevel 2>/dev/null || true)"
export AISSTACK_REVIEW_BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse HEAD 2>/dev/null || true)"
export AISSTACK_REVIEW_LIB="$SCRIPT_DIR/../lib/review-evidence.ts"
case "$(uname -s)" in
  MINGW*|MSYS*|CYGWIN*)
    if command -v cygpath >/dev/null 2>&1; then
      AISSTACK_REVIEW_LIB="$(cygpath -m "$AISSTACK_REVIEW_LIB")"
      AISSTACK_REVIEW_DIR="$(cygpath -m "$AISSTACK_REVIEW_DIR")"
    fi
    ;;
esac
export AISSTACK_REVIEW_LOG="$AISSTACK_REVIEW_DIR/$BRANCH-reviews.jsonl"

# Compute binding fields (best-effort; empty outside a git repo).
COMMIT_FULL=$(git rev-parse HEAD 2>/dev/null || true)
TREE=""
WTREE=""
DIRTY=""
if [ -n "$COMMIT_FULL" ]; then
  TREE=$(git rev-parse 'HEAD^{tree}' 2>/dev/null || true)
  # E4: the fingerprint's new objects go to a private 0700 directory this
  # operation owns (a read-only .git still fingerprints), and stay readable
  # until the consumers below (bindReview / checkSharedLibsReuse read the tree
  # and blobs) finish; removed on EXIT/INT/TERM. aisstack-wtree's one-line
  # diagnostic reaches stderr; stdout stays the hash.
  if WTREE_OBJECTS=$(mktemp -d "${TMPDIR:-/tmp}/aisstack-wtree-objects-XXXXXX" 2>/dev/null); then
    case "$(uname -s)" in
      MINGW*|MSYS*|CYGWIN*) command -v cygpath >/dev/null 2>&1 && WTREE_OBJECTS="$(cygpath -m "$WTREE_OBJECTS")" ;;
    esac
    trap 'rm -rf "$WTREE_OBJECTS"' EXIT
    trap 'exit 130' INT TERM
    WTREE=$(AISSTACK_WTREE_OBJECT_DIR="$WTREE_OBJECTS" "$SCRIPT_DIR/aisstack-wtree" || true)
    export GIT_ALTERNATE_OBJECT_DIRECTORIES="$WTREE_OBJECTS${GIT_ALTERNATE_OBJECT_DIRECTORIES:+:$GIT_ALTERNATE_OBJECT_DIRECTORIES}"
  else
    echo "aisstack-review-log: cannot create a private object directory under ${TMPDIR:-/tmp}; no working-tree fingerprint. Set TMPDIR to a writable directory." >&2
  fi
  if [ -n "$(git status --porcelain -uno 2>/dev/null | head -1)" ]; then
    DIRTY="true"
  else
    DIRTY="false"
  fi
fi

# Gate findings get `actor` from the session classifier, never from the caller;
# a non-interactive session's gate findings are forced to action "open".
SESSION_KIND=$("$SCRIPT_DIR/aisstack-session-kind" 2>/dev/null || echo interactive)
export AISSTACK_STAMP_COMMIT_FULL="$COMMIT_FULL" AISSTACK_STAMP_TREE="$TREE" AISSTACK_STAMP_WTREE="$WTREE" AISSTACK_STAMP_DIRTY="$DIRTY" AISSTACK_STAMP_SESSION_KIND="$SESSION_KIND"
if [ "$INPUT" = --start ]; then
  AISSTACK_REVIEW_SKILL="${2:-}" bun -e '
    const { captureReviewStart } = await import(process.env.AISSTACK_REVIEW_LIB);
    console.log(captureReviewStart(process.env.AISSTACK_REVIEW_SKILL));
  '
  exit $?
fi
if [ "$INPUT" = --check-shared-libs ] && [ "$#" -eq 2 ]; then
  AISSTACK_REVIEW_TOKEN="$2" bun -e '
    const { checkSharedLibsReuse } = await import(process.env.AISSTACK_REVIEW_LIB);
    console.log(JSON.stringify(checkSharedLibsReuse(JSON.parse(await Bun.stdin.text()), process.env.AISSTACK_REVIEW_TOKEN)));
  '
  exit $?
fi
usage() {
  echo 'Usage: aisstack-review-log JSON [--finish TOKEN] [--findings FILE] | --start SKILL | --check-shared-libs TOKEN < finding.json' >&2
  echo 'Exit codes: 0 written · 1 rejected · 2 usage' >&2
  exit 2
}
[ "$#" -ge 1 ] || usage
TOKEN=""
FINDINGS=""
shift
while [ "$#" -gt 0 ]; do
  case "$1" in
    --finish)   [ "$#" -ge 2 ] || usage; TOKEN="$2"; shift 2 ;;
    --findings) [ "$#" -ge 2 ] || usage; FINDINGS="$2"; shift 2 ;;
    *) usage ;;
  esac
done
case "$INPUT" in --*) usage ;; esac

# Validate (reject malformed or injection attempts) AND stamp in one pass.
# Caller values for the binding keys are dropped before stamping. A binding or
# validation failure reaches stderr as its own line (never folded into
# "invalid JSON"), and exits 1 with nothing written.
STAMPED=$(printf '%s' "$INPUT" | AISSTACK_REVIEW_TOKEN="$TOKEN" AISSTACK_REVIEW_FINDINGS="$FINDINGS" bun -e "
const { bindReview } = await import(process.env.AISSTACK_REVIEW_LIB);
let rec;
try { rec = JSON.parse(await Bun.stdin.text()); } catch (e) { console.error('aisstack-review-log: invalid JSON, skipping (' + e.message + ')'); process.exit(1); }
if (!rec || Array.isArray(rec) || typeof rec !== 'object') { console.error('aisstack-review-log: invalid JSON, skipping (expected an object)'); process.exit(1); }
try {
  console.log(JSON.stringify(bindReview(rec, process.env.AISSTACK_REVIEW_TOKEN, process.env, process.env.AISSTACK_REVIEW_FINDINGS || undefined)));
} catch (e) { console.error(e.message); process.exit(1); }
") || exit 1

echo "$STAMPED" >> "$AISSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"

# gbrain-sync: enqueue for cross-machine sync (no-op if sync is off). Plan B1:
# an unattended session never enqueues, whatever the inherited sync mode.
if [ "$("$SCRIPT_DIR/aisstack-session-kind" 2>/dev/null || echo interactive)" = "unattended" ]; then
  echo "review-log: sync skipped (unattended session)" >&2
else
  "$SCRIPT_DIR/aisstack-brain-enqueue" "projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null &
fi
