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

TOOL_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
AUDIT_ROOT="$(cd "$TOOL_ROOT/../.." && pwd -P)"
REVIEW_FILE="$TOOL_ROOT/../review-decision.json"
IMAGE8=zhct-qr-jdk8:20260909
IMAGE17=zhct-qr-jdk17:20260909
PLATFORM=linux/amd64

usage() {
  cat <<'HELP'
Usage:
  bash run.sh help | --help | check
  bash run.sh prepare [--image-set audit|android|all] [--evidence-dir ABS_PATH]
  bash run.sh versions [--image-set audit|android|all] [--evidence-dir ABS_PATH]
  bash run.sh sdkaudit --repo ABS_PATH [--aar ABS_FILE] [--evidence-dir ABS_PATH]
  bash run.sh appaudit|a02audit --repo ABS_PATH [--evidence-dir ABS_PATH]
  bash run.sh gradle --repo ABS_PATH [--evidence-dir ABS_PATH] [--offline] -- GRADLE_ARGS...

All execution commands require status APPROVED and nonempty approval_evidence in execution/review-decision.json.
help/check do not contact Docker, download tools, run Java or create containers.
prepare downloads/builds container images only after approval; it accepts SDK licenses.
versions also starts containers and therefore requires approval.
JDK 17: SDK parser/application extraction audits. JDK 8: Gradle 5.1.1 / AGP 3.4.1.
Every container uses linux/amd64, including on Apple Silicon. No host JDK/Android SDK.
Source and Git common directory are mounted read-only at their real absolute paths.
Gradle builds a temporary container copy; only reports/debug APKs leave the container.
Allowed Gradle tasks: help, tasks, --version, :app:testDebugUnitTest, :app:assembleDebug.
--tests requires a following com.cpt.zhct.weighting.{hardware,antiescape,session,diagnostics}.CLASS pattern.
--offline denies Gradle network access; caches must already contain all dependencies.
Default evidence root: execution/evidence/docker, one unique subdirectory per run.
HELP
}

fail() { printf '%s\n' "$*" >&2; exit 2; }
need_value() { [ "$#" -ge 2 ] && [ -n "$2" ] || fail "Missing value for $1"; }

command_name="${1:-help}"
case "$command_name" in
  help|--help|-h) usage; exit 0 ;;
  check)
    bash -n "$TOOL_ROOT/run.sh"
    bash -n "$TOOL_ROOT/container-task.sh"
    python3 - "$TOOL_ROOT" <<'PY'
import ast, pathlib, sys
root = pathlib.Path(sys.argv[1])
for source in sorted(root.glob('*.py')):
    ast.parse(source.read_text(), filename=str(source))
print('PASS: shell syntax and Python AST; Docker images and systems were not run.')
PY
    exit 0 ;;
  prepare|versions|sdkaudit|appaudit|a02audit|gradle) shift ;;
  *) fail "Unknown command: $command_name (use help)" ;;
esac

image_set=all
repo=''
aar=''
evidence_root="$TOOL_ROOT/../evidence/docker"
offline=0
gradle_args=()
while [ "$#" -gt 0 ]; do
  case "$1" in
    --image-set) need_value "$@"; image_set="$2"; shift 2 ;;
    --repo) need_value "$@"; repo="$2"; shift 2 ;;
    --aar) need_value "$@"; aar="$2"; shift 2 ;;
    --evidence-dir) need_value "$@"; evidence_root="$2"; shift 2 ;;
    --offline) offline=1; shift ;;
    --) shift; gradle_args=("$@"); break ;;
    --help|-h) usage; exit 0 ;;
    *) fail "Unknown argument: $1" ;;
  esac
done
case "$image_set" in audit|android|all) ;; *) fail 'Invalid --image-set' ;; esac
if [ "$command_name" = gradle ]; then
  python3 "$TOOL_ROOT/runner-checks.py" gradle-args "${gradle_args[@]}"
fi

# No bypass flag. The reviewing agent records a real user approval in this file.
python3 "$TOOL_ROOT/runner-checks.py" review "$REVIEW_FILE"

command -v docker >/dev/null || fail 'Docker CLI unavailable; do not install a host JDK as a workaround.'
docker info --format '{{.ServerVersion}}' >/dev/null

canonical_path() {
  python3 - "$1" "$2" <<'PY'
from pathlib import Path
import sys
p = Path(sys.argv[1]).expanduser()
if not p.is_absolute():
    raise SystemExit('An absolute path is required: ' + str(p))
if any(x.is_symlink() for x in [p, *p.parents]):
    raise SystemExit('Review symlink target before use: ' + str(p))
resolved = p.resolve(strict=sys.argv[2] == 'existing')
if any(x in str(resolved) for x in [',', '\n', '\r']):
    raise SystemExit('Path contains unsupported mount separator/newline')
print(resolved)
PY
}
evidence_root="$(canonical_path "$evidence_root" create)"
if [ -n "$repo" ]; then
  repo="$(canonical_path "$repo" existing)"
  [ -d "$repo" ] || fail '--repo must be a repository directory'
  repo_top="$(git -C "$repo" rev-parse --show-toplevel)"
  [ "$repo_top" = "$repo" ] || fail '--repo must be the exact repository root'
  python3 - "$repo" <<'PY'
import subprocess, sys
path = '60069db88deaa14d9e02b875/zhct/device/restaurant/billedBygram/ZhctWeightingTableYoukate'
result = subprocess.run(['git', '-C', sys.argv[1], 'remote', 'get-url', 'origin'],
                        check=True, text=True, capture_output=True)
origin = result.stdout.strip()
if origin.endswith('.git'):
    origin = origin[:-4]
if origin not in ('https://codeup.aliyun.com/' + path, 'git@codeup.aliyun.com:' + path):
    raise SystemExit('Origin identity mismatch; review the source repository before running.')
PY
  case "$evidence_root/" in "$repo/"*) fail 'Evidence must be outside the business source repository' ;; esac
fi
case "$command_name" in
  sdkaudit|appaudit|a02audit|gradle) [ -n "$repo" ] || fail '--repo is required' ;;
esac

stamp="$(date -u +%Y%m%dT%H%M%SZ)-$$"
run_dir="$evidence_root/$command_name-$stamp"
mkdir -p "$run_dir"
printf 'Evidence directory: %s\n' "$run_dir"
cp "$REVIEW_FILE" "$run_dir/review-decision.json"
finish() {
  local rc=$?
  trap - EXIT
  if [ -f "$run_dir/source-status-before.txt" ]; then
    git -C "$repo" status --porcelain > "$run_dir/source-status-after.txt" || rc=2
    git -C "$repo" rev-parse HEAD > "$run_dir/source-head-after.txt" || rc=2
    python3 "$TOOL_ROOT/runner-checks.py" snapshot "$repo" "$run_dir/source-hashes-after.json" || rc=2
    if ! cmp -s "$run_dir/source-status-before.txt" "$run_dir/source-status-after.txt" \
      || ! cmp -s "$run_dir/source-head.txt" "$run_dir/source-head-after.txt" \
      || ! cmp -s "$run_dir/source-hashes.json" "$run_dir/source-hashes-after.json"; then
      printf '%s\n' 'Source changed during execution; investigate the concurrent writer before accepting evidence.' >&2
      rc=2
    fi
  fi
  printf '%s\n' "$rc" > "$run_dir/exit-code.txt"
  printf 'Finished %s (exit %s); evidence: %s\n' "$command_name" "$rc" "$run_dir"
  exit "$rc"
}
trap finish EXIT

record_image() {
  docker image inspect "$1" --format '{{json .}}' > "$run_dir/image-$2.json"
  actual_platform="$(docker image inspect "$1" --format '{{.Os}}/{{.Architecture}}')"
  [ "$actual_platform" = "$PLATFORM" ] || fail "Wrong image architecture: $actual_platform"
}
build_image() {
  local target_image="$1" dockerfile="$2" label="$3"
  docker build --platform "$PLATFORM" --file "$TOOL_ROOT/$dockerfile" \
    --tag "$target_image" "$TOOL_ROOT" 2>&1 | tee "$run_dir/build-$label.log"
  record_image "$target_image" "$label"
}

if [ "$command_name" = prepare ]; then
  case "$image_set" in audit|all) build_image "$IMAGE17" Dockerfile.jdk17 jdk17 ;; esac
  case "$image_set" in android|all) build_image "$IMAGE8" Dockerfile.jdk8 jdk8 ;; esac
  exit 0
fi

run_options=(--rm --platform "$PLATFORM" --pull never --cap-drop ALL
  --security-opt no-new-privileges --user "$(id -u):$(id -g)"
  --env PYTHONDONTWRITEBYTECODE=1 --env GIT_OPTIONAL_LOCKS=0
  --env HOME=/tmp/zhct-home --mount "type=bind,src=$AUDIT_ROOT,dst=/audit,readonly"
  --mount "type=bind,src=$run_dir,dst=/evidence")

if [ "$command_name" = versions ]; then
  case "$image_set" in
    audit|all)
      record_image "$IMAGE17" jdk17
      docker run "${run_options[@]}" --network none "$IMAGE17" \
        bash /audit/execution/docker/container-task.sh versions17 2>&1 | tee "$run_dir/versions-jdk17.txt"
      ;;
  esac
  case "$image_set" in
    android|all)
      record_image "$IMAGE8" jdk8
      docker run "${run_options[@]}" --network none "$IMAGE8" \
        bash /audit/execution/docker/container-task.sh versions8 2>&1 | tee "$run_dir/versions-jdk8.txt"
      ;;
  esac
  exit 0
fi

# A linked worktree's .git file points to an absolute common/.git/worktrees path.
# Mount its common Git directory at the identical path, not merely the worktree.
common_raw="$(git -C "$repo" rev-parse --git-common-dir)"
case "$common_raw" in /*) common_dir="$common_raw" ;; *) common_dir="$repo/$common_raw" ;; esac
common_dir="$(canonical_path "$common_dir" existing)"
run_options+=(--mount "type=bind,src=$repo,dst=$repo,readonly"
  --workdir "$repo" --env "SOURCE_REPO=$repo"
  --env GIT_CONFIG_COUNT=1 --env GIT_CONFIG_KEY_0=safe.directory
  --env "GIT_CONFIG_VALUE_0=$repo")
case "$common_dir/" in
  "$repo/"*) ;;
  *) run_options+=(--mount "type=bind,src=$common_dir,dst=$common_dir,readonly") ;;
esac

git -C "$repo" rev-parse HEAD > "$run_dir/source-head.txt"
git -C "$repo" branch --show-current > "$run_dir/source-branch.txt"
git -C "$repo" status --porcelain > "$run_dir/source-status-before.txt"
# Hash relevant working files as well as HEAD: an implementation worktree may be dirty.
python3 "$TOOL_ROOT/runner-checks.py" snapshot "$repo" "$run_dir/source-hashes.json"

case "$command_name" in
  sdkaudit)
    aar="$(canonical_path "${aar:-$repo/app/libs/DeviceSDK-80.1.10.12.20260129.aar}" existing)"
    [ -f "$aar" ] || fail '--aar must be a file'
    record_image "$IMAGE17" jdk17
    run_options+=(--mount "type=bind,src=$aar,dst=/input/sdk.aar,readonly")
    docker run "${run_options[@]}" --network none "$IMAGE17" \
      bash /audit/execution/docker/container-task.sh sdkaudit 2>&1 | tee "$run_dir/results.txt"
    ;;
  appaudit|a02audit)
    record_image "$IMAGE17" jdk17
    docker run "${run_options[@]}" --network none "$IMAGE17" \
      bash /audit/execution/docker/container-task.sh "$command_name" \
      2> "$run_dir/stderr.txt" | tee "$run_dir/results.json"
    ;;
  gradle)
    record_image "$IMAGE8" jdk8
    # Docker named volume initialized from the image's mode-1777 directory.
    # Neither ~/.gradle nor the Mac Android SDK is mounted or modified.
    run_options+=(--mount 'type=volume,src=zhct-qr-gradle511-20260909,dst=/cache/gradle')
    if [ "$offline" = 1 ]; then run_options+=(--network none); gradle_args+=(--offline); fi
    docker run "${run_options[@]}" "$IMAGE8" \
      bash /audit/execution/docker/container-task.sh gradle "${gradle_args[@]}" 2>&1 | tee "$run_dir/gradle.log"
    ;;
esac

# The EXIT trap preserves the exit code and checks source status/HEAD/content stayed stable.
