#!/usr/bin/env bash
# scripts/preflight.sh — local reproduction of the CI gates before `git push`.
# Generated by ~/claude-config/scripts/gen-preflight.py from the workflows below;
# every command is the one CI runs. A step this file does not cover is a step
# that can only fail remotely — when a workflow step is added, regenerate or
# add it here in the same commit. ~/.claude/hooks/pre-push-preflight.sh runs
# `--quick` before every push and blocks on failure.
#
# usage: scripts/preflight.sh [--quick]   (--quick skips the test / bench suites)
set -euo pipefail
cd "$(dirname "$0")/.."

quick=0
[[ "${1:-}" == "--quick" ]] && quick=1

step() { printf '\n\033[1;34m== %s\033[0m\n' "$*"; }
# `cargo clippy` reuses fresh `cargo check` artifacts and then lints nothing;
# touching the crate roots invalidates only this repo's fingerprints.
relint() { git ls-files | grep -E '(^|/)src/(lib|main)\.rs$' | xargs -r touch; }
need() { command -v "$1" >/dev/null 2>&1 || { echo "missing tool: $1 ($2)" >&2; exit 1; }; }
has_toolchain() { rustup toolchain list | grep -q "^$1"; }

# Steps CI runs that this file cannot reproduce locally (they can only fail remotely):
#   - security-audit.yml:audit:Install cargo-audit (needs network / runner-only)
#   - security-audit.yml:deny:Install cargo-deny (needs network / runner-only)
#   - security-audit.yml:coverage:Run coverage (needs network / runner-only)
#   - fuzz.yml:fuzz:Install cargo-fuzz (needs network / runner-only)
#   - fuzz.yml:fuzz:Build fuzz target (needs network / runner-only)
#   - fuzz.yml:fuzz:Set fuzz duration (no cargo / grep)
#   - fuzz.yml:fuzz:Run fuzz target (time-boxed) (continue-on-error)
#   - fuzz.yml:fuzz:Report crash (informational) (no cargo / grep)

need actionlint "brew install actionlint"
need cargo-audit "cargo install cargo-audit --locked"
need cargo-deny "cargo install cargo-deny --locked"
need cargo-hack "cargo install cargo-hack --locked"
need cargo-machete "cargo install cargo-machete --locked"
need cargo-semver-checks "cargo install cargo-semver-checks --locked"

step "ci.yml / test: cargo fmt --check"
( export CARGO_TERM_COLOR="always"; cargo fmt --all -- --check )

step "ci.yml / test: cargo clippy (pedantic)"
relint
( export CARGO_TERM_COLOR="always"; cargo clippy --all-targets --all-features -- -W clippy::pedantic -D warnings )

step "ci.yml / no_std: Check (no_std, host)"
( export CARGO_TERM_COLOR="always"; cargo check --lib --no-default-features )

step "ci.yml / no_std: Check (no_std + simd, host)"
( export CARGO_TERM_COLOR="always"; cargo check --lib --no-default-features --features simd )

step "ci.yml / no_std: Check (no_std, bare-metal thumbv7em-none-eabihf)"
rustup target list --installed | grep -q '^thumbv7em-none-eabihf$' || rustup target add thumbv7em-none-eabihf
( export CARGO_TERM_COLOR="always"; cargo check --lib --no-default-features --target thumbv7em-none-eabihf )

step "ci.yml / no_std: Clippy (no_std, bare-metal)"
relint
rustup target list --installed | grep -q '^thumbv7em-none-eabihf$' || rustup target add thumbv7em-none-eabihf
( export CARGO_TERM_COLOR="always"; cargo clippy --lib --no-default-features --target thumbv7em-none-eabihf -- -W clippy::pedantic -D warnings )

step "ci.yml / feature-powerset: run"
( export CARGO_TERM_COLOR="always"; cargo hack check --lib --feature-powerset --depth 2 )

step "ci.yml / doc: run"
( export CARGO_TERM_COLOR="always" RUSTDOCFLAGS="-Dwarnings"; cargo doc --no-deps --all-features )

step "ci.yml / actionlint: actionlint"
actionlint .github/workflows/*.yml

step "security-audit.yml / deny: Run cargo deny check all"
( export CARGO_TERM_COLOR="always" CARGO_NET_RETRY="5" CARGO_HTTP_MULTIPLEXING="false"; cargo deny --all-features check all )

step "security-audit.yml / unused-deps: cargo machete"
cargo machete

step "security-audit.yml / stub-guard: Detect panic!(STUB) / dbg!() in src/** (block)"
(
  export CARGO_TERM_COLOR="always" CARGO_NET_RETRY="5" CARGO_HTTP_MULTIPLEXING="false"
  set -eo pipefail
  # Block: panic!(STUB...) (explicit STUB marker) + dbg!() (debug residual)
  # comment 行 (// or /*) は除外
  hits=$(grep -rnE 'panic!\([^)]*STUB|dbg!\(' \
    src/ --include="*.rs" \
    --exclude-dir=bin \
    | grep -v ':[[:space:]]*//' \
    | grep -vE ':[[:space:]]*/\*' \
    || true)
  if [ -n "$hits" ]; then
    echo "❌ panic!(STUB) / dbg!() detected in src/ (production path):"
    echo "$hits"
    echo ""
    echo "Fix: use ? operator, Result<T, E>, or remove the marker."
    echo "  See CLAUDE.md § 仮実装完了偽装の禁止ルール"
    exit 1
  fi
  echo "✓ No panic!(STUB) / dbg!() in src/ (excluding src/bin/)"
)

step "security-audit.yml / stub-guard: Detect todo!() / unimplemented!() in src/** (informational)"
(
  export CARGO_TERM_COLOR="always" CARGO_NET_RETRY="5" CARGO_HTTP_MULTIPLEXING="false"
  set -eo pipefail
  # Informational: todo!() / unimplemented!() は legitimate な fail-fast idiom
  # (CLAUDE.md「仮実装完了偽装の禁止ルール」§「未実装は必ず fail fast」)
  hits=$(grep -rnE 'todo!\(|unimplemented!\(' \
    src/ --include="*.rs" \
    --exclude-dir=bin \
    | grep -v ':[[:space:]]*//' \
    | grep -vE ':[[:space:]]*/\*' \
    || true)
  if [ -n "$hits" ]; then
    count=$(echo "$hits" | wc -l | tr -d ' ')
    echo "::warning::todo!() / unimplemented!() found in src/ ($count markers, informational):"
    echo "$hits" | head -50
  else
    echo "✓ No todo!() / unimplemented!() in src/"
  fi
)

step "security-audit.yml / stub-guard: Detect TODO / FIXME / XXX / HACK (informational)"
(
  export CARGO_TERM_COLOR="always" CARGO_NET_RETRY="5" CARGO_HTTP_MULTIPLEXING="false"
  set -eo pipefail
  hits=$(grep -rnE 'TODO|FIXME|XXX|HACK' src/ --include="*.rs" || true)
  if [ -n "$hits" ]; then
    echo "::warning::TODO/FIXME/XXX/HACK found in src/ (informational, not blocking):"
    echo "$hits" | head -50
  else
    echo "✓ No TODO/FIXME/XXX/HACK in src/"
  fi
)

step "security-audit.yml / semver-checks: Run cargo semver-checks"
(
  export CARGO_TERM_COLOR="always" CARGO_NET_RETRY="5" CARGO_HTTP_MULTIPLEXING="false"
  cargo semver-checks check-release \
    --package alice-analytics \
    --baseline-rev "HEAD~1" \
    || true
)

step "fuzz.yml / build every fuzz target (nightly; the replay needs the runner)"
if has_toolchain nightly && cargo +nightly fuzz --version >/dev/null 2>&1; then
  (cd fuzz && cargo +nightly fuzz build)
else
  echo "skip: nightly / cargo-fuzz not installed" >&2
fi

if [[ $quick -eq 1 ]]; then
  echo; echo "preflight --quick OK (test / bench suites skipped)"; exit 0
fi

step "ci.yml / test: cargo test"
( export CARGO_TERM_COLOR="always"; cargo test --all-features )

step "security-audit.yml / audit: Run cargo audit"
(
  export CARGO_TERM_COLOR="always" CARGO_NET_RETRY="5" CARGO_HTTP_MULTIPLEXING="false"
  cargo audit --deny yanked \
    --ignore RUSTSEC-2024-0436 \
    --ignore RUSTSEC-2025-0020 \
    --ignore RUSTSEC-2025-0141 \
    --ignore RUSTSEC-2026-0176 \
    --ignore RUSTSEC-2026-0177 \
    --ignore RUSTSEC-2026-0192 \
    --ignore RUSTSEC-2026-0204
)

echo; echo "preflight OK"
