#!/usr/bin/env bash
# red-team-sweep.sh — Red-team every kindergarten seed sequentially.
# For each seed: inject into OptimizerV3.sol → run red-team.sh → restore → next.
# Usage: bash red-team-sweep.sh [timeout_per_candidate]
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SEEDS_DIR="$REPO_ROOT/tools/push3-evolution/seeds"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INJECT="$REPO_ROOT/tools/push3-transpiler/inject.sh"
ATTACKS_OUT="$REPO_ROOT/onchain/script/backtesting/attacks"
PROGRESS_FILE="/tmp/red-team-sweep-progress.json"
MEMORY_FILE="$REPO_ROOT/tmp/red-team-memory.jsonl"
CROSS_PATTERNS_FILE="$REPO_ROOT/tools/red-team/cross-patterns.jsonl"
SWEEP_TSV="/tmp/sweep-results.tsv"
OPT_SOL="$REPO_ROOT/onchain/src/OptimizerV3.sol"
TIMEOUT_PER="${1:-3600}"

log() { echo "[sweep $(date -u +%H:%M:%S)] $*"; }
die() { log "FATAL: $*" >&2; exit 1; }

# ── Detect docker compose command (plugin vs legacy standalone) ────────
if docker compose version &>/dev/null 2>&1; then
  COMPOSE_CMD="docker compose"
elif command -v docker-compose &>/dev/null; then
  COMPOSE_CMD="docker-compose"
else
  die "docker compose not found. Install Docker with the compose plugin."
fi

[[ -f "$INJECT" ]] || die "inject.sh not found at $INJECT"

mkdir -p "$ATTACKS_OUT"
mkdir -p "$(dirname "$CROSS_PATTERNS_FILE")"

# Generate a unique sweep ID for traceability across runs
SWEEP_ID="sweep-$(date -u +%Y%m%d-%H%M%S)"
log "Sweep ID: $SWEEP_ID"

# Load progress
completed=()
if [[ -f "$PROGRESS_FILE" ]]; then
  while IFS= read -r line; do completed+=("$line"); done < <(jq -r '.completed[]' "$PROGRESS_FILE" 2>/dev/null || true)
fi
is_done() { for c in "${completed[@]+"${completed[@]}"}"; do [[ "$c" == "$1" ]] && return 0; done; return 1; }

# Collect named seeds only (skip run*_gen* pool entries)
seeds=()
for f in "$SEEDS_DIR"/*.push3; do
  [[ -f "$f" ]] || continue
  basename "$f" | grep -qE '^run[0-9]+_gen' && continue
  seeds+=("$f")
done
log "Found ${#seeds[@]} seeds. Timeout: ${TIMEOUT_PER}s each"
[[ ${#seeds[@]} -gt 0 ]] || die "No seeds found in $SEEDS_DIR"

# ── Smoke test: pick a random seed, inject + compile ──
SMOKE_IDX=$(( RANDOM % ${#seeds[@]} ))
SMOKE_SEED="${seeds[$SMOKE_IDX]}"
SMOKE_NAME=$(basename "$SMOKE_SEED" .push3)
log "Smoke test: $SMOKE_NAME"
cp "$OPT_SOL" "${OPT_SOL}.sweep-backup"
trap 'cp "${OPT_SOL}.sweep-backup" "$OPT_SOL" 2>/dev/null; rm -f "${OPT_SOL}.sweep-backup"; $COMPOSE_CMD -f "$REPO_ROOT/docker-compose.yml" down -v 2>/dev/null || true' EXIT

bash "$INJECT" "$SMOKE_SEED" "$OPT_SOL" || die "Smoke test inject failed for $SMOKE_NAME"
(cd "$REPO_ROOT/onchain" && forge build --silent 2>&1) || die "Smoke test compile failed for $SMOKE_NAME"
cp "${OPT_SOL}.sweep-backup" "$OPT_SOL"
log "Smoke test passed ✓"

# Write TSV header once (file persists across restarts; header only if new)
[[ ! -f "$SWEEP_TSV" ]] && \
  printf 'candidate\teth_before\teth_after\tpct_extracted\tstrategies_tried\tbest_attack\tstatus\n' > "$SWEEP_TSV"

# ── Main loop ──
for seed_file in "${seeds[@]}"; do
  seed_name=$(basename "$seed_file" .push3)
  is_done "$seed_name" && { log "SKIP $seed_name (done)"; continue; }

  log "=== RED-TEAM: $seed_name ==="

  # 1. Inject candidate into OptimizerV3.sol
  cp "${OPT_SOL}.sweep-backup" "$OPT_SOL"
  if ! bash "$INJECT" "$seed_file" "$OPT_SOL"; then
    log "SKIP $seed_name — inject failed"
    continue
  fi
  log "Injected into OptimizerV3.sol"

  # 1b. Extract optimizer profile from transpiler output (CI/AW/AS/DD constants)
  TRANSPILER_OUT="$REPO_ROOT/onchain/src/OptimizerV3Push3.sol"
  OPTIMIZER_PROFILE=$(python3 - "$TRANSPILER_OUT" <<'PYEOF'
import re, sys
try:
    with open(sys.argv[1]) as f:
        sol = f.read()
    ci_vals = set(re.findall(r'\br40\s*=\s*uint256\((\d+)\)', sol))
    aw_vals = set(re.findall(r'\br38\s*=\s*uint256\((\d+)\)', sol))
    as_vals = set(re.findall(r'\br39\s*=\s*uint256\((\d+)\)', sol))
    dd_vals = set(re.findall(r'\br37\s*=\s*uint256\((\d+)\)', sol))

    def fmt_pct(vals):
        pcts = sorted(set(round(int(v) * 100 / 1e18) for v in vals))
        return '/'.join(str(p) + '%' for p in pcts) if pcts else '?'

    def fmt_int(vals):
        ints = sorted(set(int(v) for v in vals))
        return '/'.join(str(v) for v in ints) if ints else '?'

    profile = f"CI={fmt_pct(ci_vals)}, AW={fmt_int(aw_vals)}, AS={fmt_pct(as_vals)}, DD={fmt_pct(dd_vals)}"
    # Adaptive: multiple constant branches, OR any register assigned from a variable
    has_var_assign = bool(re.search(r'\br(?:37|38|39|40)\s*=\s*uint256\s*\(\s*[a-zA-Z_]\w*\s*\)', sol))
    if len(ci_vals) > 1 or len(aw_vals) > 1 or len(as_vals) > 1 or len(dd_vals) > 1 or has_var_assign:
        profile += ", adaptive"
    print(profile)
except Exception as e:
    print(f"unknown (parse error: {e})", file=sys.stderr)
    print("unknown")
PYEOF
  )
  log "Optimizer profile: $OPTIMIZER_PROFILE"

  # 2. Clear stale attack file from previous candidate
  rm -f "$REPO_ROOT/tmp/red-team-attacks.jsonl"

  # 3. Run red-team.sh (handles bootstrap + compile + deploy + attack)
  log "Running red-team.sh (timeout: ${TIMEOUT_PER}s)..."
  set +e
  CLAUDE_TIMEOUT="$TIMEOUT_PER" CANDIDATE_NAME="$seed_name" OPTIMIZER_PROFILE="$OPTIMIZER_PROFILE" SWEEP_ID="$SWEEP_ID" \
    timeout "$((TIMEOUT_PER + 120))" \
    bash "$SCRIPT_DIR/red-team.sh" 2>&1 | tee "/tmp/red-team-${seed_name}.log"
  RED_TEAM_EXIT="${PIPESTATUS[0]}"
  set -e

  # 4. Collect attacks
  if [[ -f "$REPO_ROOT/tmp/red-team-attacks.jsonl" ]]; then
    ATTACK_COUNT=$(wc -l < "$REPO_ROOT/tmp/red-team-attacks.jsonl")
    if [[ "$ATTACK_COUNT" -gt 0 ]]; then
      cp "$REPO_ROOT/tmp/red-team-attacks.jsonl" "$ATTACKS_OUT/sweep-${seed_name}.jsonl"
      log "Saved $ATTACK_COUNT attack(s)"
    fi
  fi

  # 4b. Write one TSV row to sweep-results.tsv
  # NOTE: intentionally runs before 4c (memory clear) so strategy data is still available.
  if [[ "$RED_TEAM_EXIT" -eq 0 ]]; then
    _sweep_status="safe"
  elif [[ "$RED_TEAM_EXIT" -eq 1 ]]; then
    _sweep_status="broken"
  elif [[ "$RED_TEAM_EXIT" -eq 124 ]]; then
    _sweep_status="timeout"
  else
    _sweep_status="crashed"
  fi
  set +e
  python3 - "/tmp/red-team-${seed_name}.log" "$MEMORY_FILE" "$seed_name" "$_sweep_status" "$SWEEP_TSV" <<'PYEOF'
import re, sys, json, os

log_file  = sys.argv[1]
mem_file  = sys.argv[2]
candidate = sys.argv[3]
status    = sys.argv[4]
tsv_file  = sys.argv[5]

# Parse eth_before (first occurrence = baseline) and eth_after (last occurrence = final state)
eth_before = ""
eth_after  = ""
try:
    with open(log_file) as f:
        for line in f:
            m = re.search(r'lm_eth_before\s*[=:]\s*(\d+)', line)
            if m and not eth_before:          # first occurrence wins
                eth_before = m.group(1)
            m = re.search(r'lm_eth_after\s*[=:]\s*(\d+)', line)
            if m:                             # last occurrence wins
                eth_after = m.group(1)
except Exception as e:
    print(f"  tsv: could not read log: {e}", file=sys.stderr)

# Parse strategies from the memory file (populated by extract_memory inside red-team.sh)
strategies_tried = 0
best_attack = "none"
try:
    if os.path.isfile(mem_file) and os.path.getsize(mem_file) > 0:
        with open(mem_file) as f:
            entries = [json.loads(l) for l in f if l.strip()]
        cand_entries = [e for e in entries if e.get("candidate") == candidate]
        strategies_tried = len(set(e["strategy"] for e in cand_entries if e.get("strategy")))
        best_delta = 0
        for e in cand_entries:
            if e.get("result") == "DECREASED" and e.get("delta_bps", 0) < best_delta:
                best_delta = e["delta_bps"]
                raw = e.get("strategy", "unknown")
                best_attack = re.sub(r"\s+", "_", raw.strip()).lower()[:50]
except Exception as e:
    print(f"  tsv: could not read memory: {e}", file=sys.stderr)

# Compute pct_extracted; use sentinel when ETH values are absent (crash/early-timeout)
if not eth_before and not eth_after:
    pct_extracted = ""
else:
    pct_extracted = "0.00"
    try:
        before = int(eth_before)
        after  = int(eth_after)
        if before > 0:
            extracted = max(0, before - after)
            pct_extracted = f"{extracted * 100 / before:.2f}"
    except Exception:
        pass

# Sanitise fields: strip tabs so the row is always valid TSV
def clean(s):
    return str(s).replace("\t", " ")

row = "\t".join([
    clean(candidate), clean(eth_before), clean(eth_after),
    clean(pct_extracted), clean(strategies_tried),
    clean(best_attack), clean(status),
])
with open(tsv_file, "a") as f:
    f.write(row + "\n")
print(f"  tsv: {status} | {pct_extracted}% extracted | {strategies_tried} strategies | best={best_attack}")
PYEOF
  _py_exit=$?
  set -e
  [[ $_py_exit -ne 0 ]] && log "WARNING: TSV row write failed (exit $_py_exit) — continuing"

  # 4c. Extract abstract patterns into cross-candidate file, then clear raw memory
  if [[ -f "$MEMORY_FILE" && -s "$MEMORY_FILE" ]]; then
    set +e
    _extract_out=$(python3 - "$MEMORY_FILE" "$CROSS_PATTERNS_FILE" "$SWEEP_ID" <<'PYEOF'
import json, sys

mem_file   = sys.argv[1]
cross_file = sys.argv[2]
sweep_id   = sys.argv[3] if len(sys.argv) > 3 else "unknown"

new_entries = []
with open(mem_file) as f:
    for line in f:
        line = line.strip()
        if line:
            try:
                new_entries.append(json.loads(line))
            except Exception:
                pass

if not new_entries:
    print("No memory entries to extract")
    sys.exit(0)

# Load existing (pattern, candidate, result) keys for deduplication
existing_keys = set()
try:
    with open(cross_file) as f:
        for line in f:
            line = line.strip()
            if line:
                try:
                    e = json.loads(line)
                    existing_keys.add((e.get("pattern", ""), e.get("candidate", ""), e.get("result", "")))
                except Exception:
                    pass
except FileNotFoundError:
    pass

appended = 0
skipped = 0
with open(cross_file, 'a') as f:
    for e in new_entries:
        key = (e.get("pattern", ""), e.get("candidate", ""), e.get("result", ""))
        if key in existing_keys:
            skipped += 1
            continue
        e["sweep_id"] = sweep_id
        existing_keys.add(key)  # prevent intra-batch duplicates
        f.write(json.dumps(e) + '\n')
        appended += 1

total = appended + skipped
print(f"Extracted {appended} new entr{'y' if appended==1 else 'ies'} ({skipped} duplicate{'s' if skipped!=1 else ''} skipped) to cross-patterns file")
PYEOF
    )
    _py_exit=$?
    set -e
    [[ -n "$_extract_out" ]] && log "$_extract_out"
    [[ $_py_exit -ne 0 ]] && log "WARNING: cross-pattern extraction failed (exit $_py_exit) — continuing"
  fi
  # Always clear raw memory so the next candidate starts with a fresh tactical state
  if [[ -f "$MEMORY_FILE" ]]; then
    > "$MEMORY_FILE"
    log "Cleared raw memory for next candidate"
  fi

  # 5. Save progress
  completed+=("$seed_name")
  jq -n --argjson arr "$(printf '%s\n' "${completed[@]}" | jq -R . | jq -s .)" \
    '{completed: $arr, last_updated: now | todate}' > "$PROGRESS_FILE"
  log "DONE $seed_name"

  # 6. Teardown — poll until all containers have exited (exponential backoff, no fixed delay)
  $COMPOSE_CMD -f "$REPO_ROOT/docker-compose.yml" down -v 2>/dev/null || true
  _deadline=$(( $(date +%s) + 30 ))
  _backoff_ms=100
  while [[ -n "$($COMPOSE_CMD -f "$REPO_ROOT/docker-compose.yml" ps --quiet 2>/dev/null)" ]]; do
    if [[ $(date +%s) -ge $_deadline ]]; then
      log "WARNING: containers still present after 30s — proceeding anyway"
      break
    fi
    sleep "$(awk "BEGIN{printf \"%.1f\", $_backoff_ms / 1000}")"
    (( _backoff_ms = _backoff_ms * 2 > 2000 ? 2000 : _backoff_ms * 2 ))
  done
done

# Restore original
cp "${OPT_SOL}.sweep-backup" "$OPT_SOL"
log "=== SWEEP COMPLETE: ${#completed[@]} / ${#seeds[@]} ==="
