#!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'EOF'
Usage: autoreview [options]

Options:
  --mode auto|local|branch|commit
      Target selection. Default: auto.
  --base REF
      Base ref for branch review. Default: PR base or origin/main.
  --commit REF
      Commit ref for commit review. Default: HEAD.
  --reviewer codex|claude|pi|opencode|droid|copilot|auto
      Review engine. Default: Codex with configured fallback on error.
  --fallback-reviewer auto|claude|pi|opencode|droid|copilot|none
      Fallback when Codex is unavailable or exits nonzero without findings.
  --codex-bin PATH
      Codex binary. Default: codex.
  --claude-bin PATH
      Claude binary. Default: claude.
  --pi-bin PATH
      Pi binary. Default: pi.
  --opencode-bin PATH
      OpenCode binary. Default: opencode.
  --droid-bin PATH
      Droid binary. Default: droid.
  --copilot-bin PATH
      GitHub Copilot binary. Default: copilot.
  --full-access
      Keep yolo/full-access mode enabled. Default.
  --no-yolo
      Run nested Codex review with normal sandbox/approval prompts.
  --output FILE
      Also save output to file.
  --parallel-tests CMD
      Run review and test command concurrently. Pass "" to disable auto-tests.
      Default: bun run ci:static when package.json, bun.lock, node_modules, and
      a ci:static script are present.
  --dry-run
      Print selected commands, do not run.
  -h, --help
      Show help.

Modes:
  local   codex review --uncommitted
  branch  codex review --base <ref>
  commit  codex review --commit <ref>
  auto    dirty tree -> local, else PR/current branch -> branch
EOF
}

mode=auto
base_ref=
commit_ref=HEAD
reviewer=${AUTOREVIEW_REVIEWER:-${CODEX_REVIEW_REVIEWER:-auto}}
fallback_reviewer=${AUTOREVIEW_FALLBACK_REVIEWER:-${CODEX_REVIEW_FALLBACK_REVIEWER:-auto}}
codex_bin=${CODEX_BIN:-codex}
claude_bin=${CLAUDE_BIN:-claude}
pi_bin=${PI_BIN:-pi}
opencode_bin=${OPENCODE_BIN:-opencode}
droid_bin=${DROID_BIN:-droid}
copilot_bin=${COPILOT_BIN:-copilot}
yolo=${AUTOREVIEW_YOLO:-${CODEX_REVIEW_YOLO:-1}}
output=${AUTOREVIEW_OUTPUT:-${CODEX_REVIEW_OUTPUT:-}}
parallel_tests=
parallel_tests_set=false
parallel_tests_auto=false
dry_run=false

while [[ $# -gt 0 ]]; do
  case "$1" in
    --mode) mode=${2:-}; shift 2 ;;
    --base) base_ref=${2:-}; shift 2 ;;
    --commit) commit_ref=${2:-}; shift 2 ;;
    --reviewer) reviewer=${2:-}; shift 2 ;;
    --fallback-reviewer) fallback_reviewer=${2:-}; shift 2 ;;
    --codex-bin) codex_bin=${2:-}; shift 2 ;;
    --claude-bin) claude_bin=${2:-}; shift 2 ;;
    --pi-bin) pi_bin=${2:-}; shift 2 ;;
    --opencode-bin) opencode_bin=${2:-}; shift 2 ;;
    --droid-bin) droid_bin=${2:-}; shift 2 ;;
    --copilot-bin) copilot_bin=${2:-}; shift 2 ;;
    --full-access) yolo=1; shift ;;
    --no-yolo) yolo=0; shift ;;
    --output) output=${2:-}; shift 2 ;;
    --parallel-tests) parallel_tests=${2:-}; parallel_tests_set=true; shift 2 ;;
    --dry-run) dry_run=true; shift ;;
    -h|--help) usage; exit 0 ;;
    *) usage >&2; exit 2 ;;
  esac
done

case "$mode" in
  auto|local|branch|commit) ;;
  *) echo "invalid --mode: $mode" >&2; exit 2 ;;
esac

case "$reviewer" in
  auto|codex|claude|pi|opencode|droid|copilot) ;;
  *) echo "invalid --reviewer: $reviewer" >&2; exit 2 ;;
esac

case "$fallback_reviewer" in
  auto|claude|pi|opencode|droid|copilot|none) ;;
  *) echo "invalid --fallback-reviewer: $fallback_reviewer" >&2; exit 2 ;;
esac

repo_root=$(git rev-parse --show-toplevel)
current_branch=$(git branch --show-current 2>/dev/null || true)
dirty=false
if [[ -n "$(git status --porcelain)" ]]; then
  dirty=true
fi

codex_args=()
case "$yolo" in
  0|false|False|FALSE|no|No|NO|off|Off|OFF) ;;
  *) codex_args+=(--dangerously-bypass-approvals-and-sandbox --sandbox danger-full-access) ;;
esac

has_package_script() {
  local script_name=$1
  command -v node >/dev/null 2>&1 || return 1
  node -e '
    const { readFileSync } = require("node:fs");
    const pkg = JSON.parse(readFileSync(process.argv[1], "utf8"));
    process.exit(pkg.scripts?.[process.argv[2]] ? 0 : 1);
  ' "$repo_root/package.json" "$script_name" >/dev/null 2>&1
}

auto_tests_disabled() {
  case "${AUTOREVIEW_AUTO_TESTS:-${CODEX_REVIEW_AUTO_TESTS:-1}}" in
    0|false|False|FALSE|no|No|NO|off|Off|OFF) return 0 ;;
    *) return 1 ;;
  esac
}

pr_url=
if [[ -z "$base_ref" && "$mode" != local ]] && command -v gh >/dev/null 2>&1; then
  if pr_lines=$(gh pr view --json baseRefName,url --jq '[.baseRefName, .url] | @tsv' 2>/dev/null); then
    base_name=${pr_lines%%$'\t'*}
    pr_url=${pr_lines#*$'\t'}
    if [[ -n "$base_name" ]]; then
      base_ref="origin/$base_name"
    fi
  fi
fi

if [[ -z "$base_ref" ]]; then
  base_ref=origin/main
fi

review_kind=
if [[ "$mode" == local || ( "$mode" == auto && "$dirty" == true ) ]]; then
  review_kind=local
elif [[ "$mode" == commit ]]; then
  review_kind=commit
elif [[ "$mode" == branch || ( "$mode" == auto && -n "$current_branch" && "$current_branch" != "main" ) ]]; then
  review_kind=branch
else
  echo "no review target: clean main checkout and no forced mode" >&2
  exit 1
fi

if [[ "$review_kind" == local ]]; then
  review_cmd=("$codex_bin" "${codex_args[@]}" review --uncommitted)
elif [[ "$review_kind" == commit ]]; then
  review_cmd=("$codex_bin" "${codex_args[@]}" review --commit "$commit_ref")
else
  review_cmd=("$codex_bin" "${codex_args[@]}" review --base "$base_ref")
fi

if [[ "$parallel_tests_set" == false && -z "$parallel_tests" ]] && ! auto_tests_disabled; then
  if [[ -f "$repo_root/package.json" && -f "$repo_root/bun.lock" && -d "$repo_root/node_modules" ]] &&
    command -v bun >/dev/null 2>&1 && has_package_script ci:static; then
    printf -v quoted_repo_root '%q' "$repo_root"
    parallel_tests="cd $quoted_repo_root && bun run ci:static"
    parallel_tests_auto=true
  fi
fi

printf 'autoreview target: %s\n' "$review_kind"
printf 'branch: %s\n' "${current_branch:-detached}"
if [[ -n "$pr_url" ]]; then
  printf 'pr: %s\n' "$pr_url"
fi
if [[ "$reviewer" == auto ]]; then
  printf 'reviewer: codex\n'
else
  printf 'reviewer: %s\n' "$reviewer"
fi
if [[ "$reviewer" == auto || "$reviewer" == codex ]]; then
  printf 'review:'
  printf ' %q' "${review_cmd[@]}"
  printf '\n'
else
  printf 'review: %s prompt review\n' "$reviewer"
fi
if [[ -n "$parallel_tests" ]]; then
  printf 'tests: %s' "$parallel_tests"
  if [[ "$parallel_tests_auto" == true ]]; then
    printf ' (auto)'
  fi
  printf '\n'
fi
if [[ "$review_kind" == branch ]]; then
  printf 'fetch: git fetch origin --quiet\n'
fi
if [[ -n "$output" ]]; then
  printf 'output: %s\n' "$output"
fi
if [[ "$dry_run" == true ]]; then
  exit 0
fi

if [[ "$review_kind" == branch ]]; then
  git fetch origin --quiet || {
    echo "warning: git fetch origin failed; reviewing with existing refs" >&2
  }
fi

review_output=$output
review_output_is_temp=false
prompt_file=
if [[ -z "$review_output" ]]; then
  review_output=$(mktemp)
  review_output_is_temp=true
fi
mkdir -p "$(dirname "$review_output")"
: > "$review_output"

cleanup() {
  if [[ "${review_output_is_temp:-false}" == true && -n "${review_output:-}" ]]; then
    rm -f "$review_output"
  fi
  if [[ -n "${prompt_file:-}" ]]; then
    rm -f "$prompt_file"
  fi
}
trap cleanup EXIT

diff_for_review() {
  case "$review_kind" in
    local)
      git -C "$repo_root" diff --stat
      git -C "$repo_root" diff --cached --stat
      git -C "$repo_root" diff --find-renames
      git -C "$repo_root" diff --cached --find-renames
      while IFS= read -r untracked_file; do
        [[ -n "$untracked_file" ]] || continue
        git -C "$repo_root" diff --no-index -- /dev/null "$untracked_file" || true
      done < <(git -C "$repo_root" ls-files --others --exclude-standard)
      ;;
    commit)
      git -C "$repo_root" show --find-renames --stat --format=fuller "$commit_ref"
      git -C "$repo_root" show --find-renames --format=medium "$commit_ref"
      ;;
    branch)
      git -C "$repo_root" diff --find-renames --stat "$base_ref"...HEAD
      git -C "$repo_root" diff --find-renames "$base_ref"...HEAD
      ;;
  esac
}

build_prompt_file() {
  prompt_file=$(mktemp)
  {
    cat <<'EOF'
You are reviewing a ClawHub diff.

Return only accepted/actionable findings. Verify claims against the diff and
reject speculative, low-value, or overbroad rewrites.

Use this format for findings:
[P1] Short title
File: path:line
Why: one sentence
Fix: one sentence

If no accepted/actionable findings, output exactly:
autoreview clean: no accepted/actionable findings reported

Diff:
EOF
    diff_for_review
  } > "$prompt_file"
}

review_output_has_clean_marker() {
  local path=$1
  grep -Eq '^[^[:alnum:]]*autoreview clean: no accepted/actionable findings reported[[:space:]]*$' "$path"
}

review_output_has_findings() {
  grep -Eq '\[P[0-3]\]' "$review_output"
}

review_output_empty() {
  [[ ! -s "$review_output" ]] || ! grep -q '[^[:space:]]' "$review_output"
}

review_output_used_prompt_reviewer() {
  grep -Eq '^fallback: (claude -p|pi -p|opencode run|droid exec|copilot)$' "$review_output"
}

run_codex_review() {
  if ! command -v "$codex_bin" >/dev/null 2>&1; then
    echo "codex reviewer unavailable: $codex_bin" >&2
    return 127
  fi
  "${review_cmd[@]}" 2>&1 | tee "$review_output"
}

run_prompt_reviewer() {
  local selected=$1
  local status=0
  local prompt_bytes=0
  local copilot_prompt=
  build_prompt_file
  case "$selected" in
    claude)
      command -v "$claude_bin" >/dev/null 2>&1 || {
        echo "fallback reviewer unavailable: $claude_bin" >&2
        return 127
      }
      printf 'fallback: claude -p\n' | tee -a "$review_output"
      "$claude_bin" --tools "" --no-session-persistence -p < "$prompt_file" 2>&1 | tee -a "$review_output"
      status=${PIPESTATUS[0]}
      ;;
    pi)
      command -v "$pi_bin" >/dev/null 2>&1 || {
        echo "fallback reviewer unavailable: $pi_bin" >&2
        return 127
      }
      printf 'fallback: pi -p\n' | tee -a "$review_output"
      "$pi_bin" --no-tools --no-session -p < "$prompt_file" 2>&1 | tee -a "$review_output"
      status=${PIPESTATUS[0]}
      ;;
    opencode)
      command -v "$opencode_bin" >/dev/null 2>&1 || {
        echo "fallback reviewer unavailable: $opencode_bin" >&2
        return 127
      }
      printf 'fallback: opencode run\n' | tee -a "$review_output"
      "$opencode_bin" run --pure --dir "$repo_root" "Review the attached prompt file. Do not modify files." --file "$prompt_file" 2>&1 | tee -a "$review_output"
      status=${PIPESTATUS[0]}
      ;;
    droid)
      command -v "$droid_bin" >/dev/null 2>&1 || {
        echo "fallback reviewer unavailable: $droid_bin" >&2
        return 127
      }
      printf 'fallback: droid exec\n' | tee -a "$review_output"
      "$droid_bin" exec --cwd "$repo_root" -f "$prompt_file" 2>&1 | tee -a "$review_output"
      status=${PIPESTATUS[0]}
      ;;
    copilot)
      command -v "$copilot_bin" >/dev/null 2>&1 || {
        echo "fallback reviewer unavailable: $copilot_bin" >&2
        return 127
      }
      printf 'fallback: copilot\n' | tee -a "$review_output"
      prompt_bytes=$(wc -c < "$prompt_file" | tr -d '[:space:]')
      if (( prompt_bytes > 120000 )); then
        echo "copilot reviewer unavailable: generated prompt is too large" | tee -a "$review_output"
        status=1
      else
        copilot_prompt=$(< "$prompt_file")
        "$copilot_bin" -C "$repo_root" --available-tools=none --stream off --output-format text --silent -p "$copilot_prompt" 2>&1 | tee -a "$review_output"
        status=${PIPESTATUS[0]}
      fi
      ;;
    *)
      echo "unsupported prompt reviewer: $selected" >&2
      status=2
      ;;
  esac
  rm -f "$prompt_file"
  prompt_file=
  return "$status"
}

fallback_reviewer_is_available() {
  local selected=$1
  case "$selected" in
    claude) command -v "$claude_bin" >/dev/null 2>&1 ;;
    pi) command -v "$pi_bin" >/dev/null 2>&1 ;;
    opencode) command -v "$opencode_bin" >/dev/null 2>&1 ;;
    droid) command -v "$droid_bin" >/dev/null 2>&1 ;;
    copilot) command -v "$copilot_bin" >/dev/null 2>&1 ;;
    *) return 1 ;;
  esac
}

run_selected_review() {
  local selected=$1
  case "$selected" in
    codex) run_codex_review ;;
    claude|pi|opencode|droid|copilot) run_prompt_reviewer "$selected" ;;
    *) echo "unsupported reviewer: $selected" >&2; return 2 ;;
  esac
}

run_auto_fallback_review() {
  local selected
  if [[ "$fallback_reviewer" != auto ]]; then
    run_selected_review "$fallback_reviewer"
    return $?
  fi
  for selected in claude pi opencode droid copilot; do
    if fallback_reviewer_is_available "$selected"; then
      run_selected_review "$selected"
      return $?
    fi
  done
  echo "fallback reviewer unavailable: no configured fallback CLI found" >&2
  return 127
}

run_auto_review() {
  local status=0
  run_selected_review codex
  status=$?
  if [[ "$status" == 0 ]]; then
    return 0
  fi
  if (( status > 128 && status < 192 )); then
    return "$status"
  fi
  if review_output_has_findings; then
    return "$status"
  fi
  if [[ "$fallback_reviewer" == none ]]; then
    return "$status"
  fi
  if [[ "$fallback_reviewer" == auto ]]; then
    printf 'autoreview warning: codex exited %s; trying configured fallback reviewers\n' "$status" >&2
  else
    printf 'autoreview warning: codex exited %s; falling back to %s\n' "$status" "$fallback_reviewer" >&2
  fi
  run_auto_fallback_review
}

elapsed_since() {
  local started_at=$1
  local finished_at
  finished_at=$(date +%s)
  printf '%s\n' "$((finished_at - started_at))"
}

format_elapsed() {
  local seconds=$1
  if (( seconds < 60 )); then
    printf '%ss\n' "$seconds"
  else
    printf '%sm%ss\n' "$((seconds / 60))" "$((seconds % 60))"
  fi
}

report_clean_review_or_fail() {
  local elapsed_text
  elapsed_text=$(format_elapsed "${review_elapsed_seconds:-0}")
  if review_output_has_findings; then
    printf 'autoreview complete after %s\n' "$elapsed_text"
    printf 'autoreview findings: accepted/actionable findings reported\n'
    return 1
  fi
  if review_output_empty; then
    printf 'autoreview complete after %s; no output\n' "$elapsed_text"
    return 1
  fi
  if review_output_used_prompt_reviewer && ! review_output_has_clean_marker "$review_output"; then
    printf 'autoreview complete after %s\n' "$elapsed_text"
    printf 'autoreview findings: prompt reviewer did not emit clean marker\n'
    return 1
  fi
  printf 'autoreview complete after %s\n' "$elapsed_text"
  printf 'autoreview clean: no accepted/actionable findings reported\n'
}

if [[ -z "$parallel_tests" ]]; then
  review_started_at=$(date +%s)
  set +e
  if [[ "$reviewer" == auto ]]; then
    run_auto_review
  else
    run_selected_review "$reviewer"
  fi
  review_status=$?
  review_elapsed_seconds=$(elapsed_since "$review_started_at")
  set -e
  if [[ "$review_status" == 0 ]]; then
    report_clean_review_or_fail
    exit $?
  fi
  exit "$review_status"
fi

review_status_file=$(mktemp)
review_elapsed_file=$(mktemp)
tests_status_file=$(mktemp)

(
  set +e
  review_started_at=$(date +%s)
  if [[ "$reviewer" == auto ]]; then
    run_auto_review
  else
    run_selected_review "$reviewer"
  fi
  status=$?
  elapsed=$(elapsed_since "$review_started_at")
  printf '%s\n' "$status" > "$review_status_file"
  printf '%s\n' "$elapsed" > "$review_elapsed_file"
) &
review_pid=$!

(
  set +e
  bash -lc "$parallel_tests"
  status=$?
  printf '%s\n' "$status" > "$tests_status_file"
) &
tests_pid=$!

wait "$review_pid" || true
wait "$tests_pid" || true

review_status=$(cat "$review_status_file")
review_elapsed_seconds=$(cat "$review_elapsed_file")
tests_status=$(cat "$tests_status_file")
rm -f "$review_status_file" "$review_elapsed_file" "$tests_status_file"

printf 'autoreview exit: %s\n' "$review_status"
printf 'tests exit: %s\n' "$tests_status"

if [[ "$review_status" != 0 || "$tests_status" != 0 ]]; then
  exit 1
fi

report_clean_review_or_fail
