#!/usr/bin/env bash
#
# Fixture UART ↔ Ethernet bridge — all four positions (daisy-chain).
#
# Routing (every Pi):
#   UART origin     → every carrier-up ETH port (both legs when both cabled)
#   ETH ingress     → peer port(s) + UART inject (optional FIXTURE_ETH_RELAY_ONLY=1 skips inject)
#
# Env:
#   FIXTURE_POS=1|2|3|4   (required)
#   PI_LABEL=topbox3
#   CHANNEL=machine|ois|both   both = dual UART bridge (two uartd instances; EG 2.2)
#   JUN11_ETH_LEGACY=1     use --interface br0 (run_bridge_jun11_br0_legacy.sh only)
#   ETH_PORTS=eth0,eth1    directional routing (default milestone stack)
#   FIXTURE_UART_ETH_EGRESS=   optional single-port override (default: fan-out all live ports)
#   FIXTURE_ETH_LIVE_PORTS_ONLY=1   skip ports with carrier down (default on)
#   FIXTURE_ETH_RELAY_ONLY=0   set 1 only for wire relay with no local game
#
#   POST_TX_US=300
#   POST_AIR_MARGIN_US=200
#   POST_HS_INACTIVE_US=150
#   HS_INACTIVE_WAIT_MS=80
#   WAIT_HS_INACTIVE=1
#   FLUSH_RX_BEFORE_TX=0
#   FIXTURE_DIR=
#   VERBOSE=1
#   CLEAR_CAPTURE=1     default: wipe data/ and logs/ contents on start (incl. rotates)
#   SUPPRESS_UART_ECHO=1      suppress ETH reflection of local UART→ETH (off by default; good 10am session had this off)
#   FIXTURE_STATUS_0D_FIRST_ONLY=1   ETH→UART: inject only first 0x0D per game+counter
#   FIXTURE_STATUS_0D_BLOCK_ALL=1    ETH→UART: block ALL 0x0D close (required for synth)
#   FIXTURE_STATUS_0D_SYNTH=1        cache blocked 0x0D; inject after x1 ACK solo or glued (jun11-good default)
#   FIXTURE_STATUS_0D_FIRST_PASSTHROUGH=1  per game: first Master 0x0D passes through (seeds template)
#   FIXTURE_STATUS_0D_SYNTH_FROM_X9=1  build 0x0D from wager+x9 at x1 (default ON when SYNTH=1)
#   FIXTURE_STATUS_0D_SYNTH_LOOSE_COUNTER=1  with synth-from-x9: use counter=0 (session may not matter)
#   FIXTURE_STATUS_0D_DEDUPE_MS=5000 dedupe key TTL when first-only (ms)
#   FIXTURE_LAB_KENO_JPOT_SYNTH=0   LAB ONLY pos3 Master: hijack 0x49 on POG422 → synth → both ETH
#   FIXTURE_LAB_KENO_JPOT_GAME=9309  TouchEasy Keno bus id (default 9309)
#   FIXTURE_LAB_KENO_JPOT_AMOUNT=    optional LE32 progwinval (hex ok: 0x7A139)
#   FIXTURE_LAB_KENO_JPOT_TID=       optional slave TID filter (e.g. pos1 terminal)
#   FIXTURE_LAB_POKER_ROYAL_SYNTH=0  LAB ONLY Master: hijack 0x59/0x5A on POG422 → synth → ETH
#   FIXTURE_LAB_POKER_GAME=9400      Jacks or Better bus id (default 9400)
#   FIXTURE_LAB_POKER_JPOT_AMOUNT=   optional BE32 progwinval for 0x5A (cents; hex ok)
#   FIXTURE_LAB_POKER_TID=           optional slave TID filter
#   FIXTURE_LAB_TOUCH6_JPOT_SYNTH=0  LAB ONLY Master: hijack 0x29 → draw 1-6 + jpot from wager
#   FIXTURE_LAB_TOUCH6_JPOT_GAME=9107  Touch 6 Lotto bus id (default 9107, wire 07,91)
#   FIXTURE_LAB_TOUCH6_JPOT_AMOUNT=   optional BE32 progwinval (cents; hex ok)
#   FIXTURE_LAB_TOUCH6_JPOT_TID=      optional slave TID filter
#   FIXTURE_LAB_SUPERPICK_JPOT_SYNTH=0  LAB ONLY Master: hijack 0x69 → draw 0-5 + jpot from wager
#   FIXTURE_LAB_SUPERPICK_JPOT_GAME=9507  SuperPick bus id (default 9507, wire 07,95)
#   FIXTURE_LAB_SUPERPICK_JPOT_AMOUNT=   optional BE32 progwinval (cents; hex ok)
#   FIXTURE_LAB_SUPERPICK_JPOT_TID=      optional slave TID filter
#   FIXTURE_LAB_BINGO_JPOT_SYNTH=0  LAB ONLY Master: hijack 0x39 → coverall + forced progwin
#   FIXTURE_LAB_BINGO_JPOT_GAME=9206  SuperGold Bingo bus id (default 9206, wire 06,92)
#   FIXTURE_LAB_BINGO_JPOT_AMOUNT=   optional BE32 progwinval (cents; default LP $7488.50)
#   FIXTURE_LAB_BINGO_JPOT_TID=      optional slave TID filter
#   FIXTURE_LAB_TRIPLE7S_JPOT_SYNTH=0  LAB ONLY Master: hijack 0x89 → 3× TRIPLE7 payline 1
#   FIXTURE_LAB_TRIPLE7S_JPOT_GAME=970C  Triple 7's bus id (default 970C, wire 0C,97)
#   FIXTURE_LAB_TRIPLE7S_JPOT_TID=      optional slave TID filter
#   FIXTURE_LAB_WITHHOLD_STATUS_0D=0  LAB ONLY: withhold local synth/auto-close 0x0D (Slave MRT)
#   FIXTURE_LAB_WITHHOLD_STATUS_0D_COUNT=1  rounds to withhold (default 1)
#   LAB_WITHHOLD_STATUS_0D / LAB_WITHHOLD_STATUS_0D_COUNT  aliases for the above
#   LAB_GAMEPLAY_SYNTH=0               LAB ONLY: forge rotating multi-game slave wagers (UART→ETH)
#   LAB_GAMEPLAY_TID=E001              required when synth on (lab range E000–E0FF)
#   LAB_GAMEPLAY_GAMES=touch6,superpick,bingo,poker
#   LAB_GAMEPLAY_SESSION_S=300         seconds per game session
#   LAB_GAMEPLAY_PERIOD_S=3            round cadence inside session
#   LAB_GAMEPLAY_JITTER_S=0.5
#   LAB_GAMEPLAY_PHASE_S=              optional hop stagger (default: hash of FIXTURE_POS)
#   LAB_GAMEPLAY_SHUFFLE=0
#   LAB_GAMEPLAY_SYNTH_ALLOW_PROD=0    never set in field
#   FIXTURE_EARLY_WIN_PHASE1=0  Phase 1: progwinval trap → DyzineFrame /flag/fireworks (bank-wide)
#   FIXTURE_EARLY_WIN_DRY_RUN=0   log early_win_phase1_* only; skip HTTP trigger
#   DYZINEFRAME_FLAG_BASE=http://127.0.0.1:31234
#   FIXTURE_SCR_DIAG=0               log scr_diag/scr_round for native UART 0x03/0x0B
#   FIXTURE_SCR_MASTER_NATIVE_ONLY=0 skip relayed 0x03 ETH→inject (Master SCR bench)
#   FIXTURE_HS_WITNESS=1             live console BSY empty-window counter (default on)
#   FIXTURE_LINK_WATCH=1      background ARP/carrier watch (jun11-good default on)
#   POG422_UARTD=1            EG 2.2 default: pog422_uartd owns UART (required)
#   POG422_UARTD=0            unsupported on wide/preciseflags drivers (hard-fail)
#   POG422_UARTD_INJECT=1     EG 2.2 default: C INJECT_TX + require_idle (required)
#   POG422_UARTD_INJECT=0     unsupported — Python MARK/SPACE removed
#   POG422_UARTD_SOCK=        override /tmp/pog422_uart_<pos>.sock (single channel)
#   POG422_UARTD_SOCK_MACHINE / POG422_UARTD_SOCK_OIS  CHANNEL=both sockets
#   LINK_WATCH_INTERVAL=1.0   seconds between probes
#   LINK_RECOVER=1            enable auto soft recover on sustained link_arp_fail
#   LINK_RECOVER_AFTER=5      consecutive ARP fails before soft recover (only when LINK_RECOVER=1)
#   LINK_RECOVER_COOLDOWN=15  seconds between soft recover attempts
#   LINK_RECOVER_HARD_AFTER=3 failed soft recoveries → br0 hard reinit + bridge restart
#
# Position 4 (machine bridge) uses tighter defaults — override any variable to tune.

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Flat layout: uartd_common.sh + inject-shim/ live beside this script (was parent dir).
TIMING_ROOT="${ROOT}"
cd "${ROOT}"

# shellcheck source=cpu_layout.sh
if [[ -f "${TIMING_ROOT}/cpu_layout.sh" ]]; then
  source "${TIMING_ROOT}/cpu_layout.sh"
fi
# shellcheck source=uartd_common.sh
source "${TIMING_ROOT}/uartd_common.sh"

FIXTURE_POS="${FIXTURE_POS:-}"
if [[ -z "${FIXTURE_POS}" ]] || ! [[ "${FIXTURE_POS}" =~ ^[1-9][0-9]?$ ]]; then
  echo "FATAL: set FIXTURE_POS=1..99 (1–5 seats; 51 = pos5 POG422 witness)" >&2
  exit 1
fi

PI_LABEL="${PI_LABEL:-topbox${FIXTURE_POS}}"
CHANNEL="${CHANNEL:-machine}"
JUN11_ETH_LEGACY="${JUN11_ETH_LEGACY:-0}"
ETH_PORTS="${ETH_PORTS:-eth0,eth1}"
INTERFACE="${INTERFACE:-br0}"
LINK_WATCH_INTERFACE="${LINK_WATCH_INTERFACE:-eth1}"
OUTPUT_DIR="${OUTPUT_DIR:-./data}"
LOG_DIR="${LOG_DIR:-./logs}"
VERBOSE="${VERBOSE:-0}"
FIXTURE_DIR="${FIXTURE_DIR:-}"
FLUSH_RX_BEFORE_TX="${FLUSH_RX_BEFORE_TX:-0}"
WAIT_HS_INACTIVE="${WAIT_HS_INACTIVE:-1}"
CLEAR_CAPTURE="${CLEAR_CAPTURE:-1}"
SUPPRESS_UART_ECHO="${SUPPRESS_UART_ECHO:-0}"
FIXTURE_STATUS_0D_FIRST_ONLY="${FIXTURE_STATUS_0D_FIRST_ONLY:-0}"
FIXTURE_STATUS_0D_BLOCK_ALL="${FIXTURE_STATUS_0D_BLOCK_ALL:-0}"
FIXTURE_STATUS_0D_SYNTH="${FIXTURE_STATUS_0D_SYNTH:-0}"
FIXTURE_STATUS_0D_FIRST_PASSTHROUGH="${FIXTURE_STATUS_0D_FIRST_PASSTHROUGH:-0}"
FIXTURE_STATUS_0D_SYNTH_FROM_X9="${FIXTURE_STATUS_0D_SYNTH_FROM_X9:-0}"
FIXTURE_STATUS_0D_SYNTH_LOOSE_COUNTER="${FIXTURE_STATUS_0D_SYNTH_LOOSE_COUNTER:-0}"
FIXTURE_STATUS_0D_DEDUPE_MS="${FIXTURE_STATUS_0D_DEDUPE_MS:-5000}"
FIXTURE_LAB_KENO_JPOT_SYNTH="${FIXTURE_LAB_KENO_JPOT_SYNTH:-0}"
FIXTURE_LAB_KENO_JPOT_GAME="${FIXTURE_LAB_KENO_JPOT_GAME:-9309}"
FIXTURE_LAB_KENO_JPOT_AMOUNT="${FIXTURE_LAB_KENO_JPOT_AMOUNT:-}"
FIXTURE_LAB_KENO_JPOT_TID="${FIXTURE_LAB_KENO_JPOT_TID:-}"
FIXTURE_LAB_POKER_ROYAL_SYNTH="${FIXTURE_LAB_POKER_ROYAL_SYNTH:-0}"
FIXTURE_LAB_POKER_GAME="${FIXTURE_LAB_POKER_GAME:-9400}"
FIXTURE_LAB_POKER_JPOT_AMOUNT="${FIXTURE_LAB_POKER_JPOT_AMOUNT:-}"
FIXTURE_LAB_POKER_TID="${FIXTURE_LAB_POKER_TID:-}"
FIXTURE_LAB_SPINJACK_FLUSHJACK_SYNTH="${FIXTURE_LAB_SPINJACK_FLUSHJACK_SYNTH:-0}"
FIXTURE_LAB_SPINJACK_GAME="${FIXTURE_LAB_SPINJACK_GAME:-960F}"
FIXTURE_LAB_SPINJACK_TID="${FIXTURE_LAB_SPINJACK_TID:-}"
FIXTURE_LAB_TOUCH6_JPOT_SYNTH="${FIXTURE_LAB_TOUCH6_JPOT_SYNTH:-0}"
FIXTURE_LAB_TOUCH6_JPOT_GAME="${FIXTURE_LAB_TOUCH6_JPOT_GAME:-9107}"
FIXTURE_LAB_TOUCH6_JPOT_AMOUNT="${FIXTURE_LAB_TOUCH6_JPOT_AMOUNT:-}"
FIXTURE_LAB_TOUCH6_JPOT_TID="${FIXTURE_LAB_TOUCH6_JPOT_TID:-}"
FIXTURE_LAB_SUPERPICK_JPOT_SYNTH="${FIXTURE_LAB_SUPERPICK_JPOT_SYNTH:-0}"
FIXTURE_LAB_SUPERPICK_JPOT_GAME="${FIXTURE_LAB_SUPERPICK_JPOT_GAME:-9507}"
FIXTURE_LAB_SUPERPICK_JPOT_AMOUNT="${FIXTURE_LAB_SUPERPICK_JPOT_AMOUNT:-}"
FIXTURE_LAB_SUPERPICK_JPOT_TID="${FIXTURE_LAB_SUPERPICK_JPOT_TID:-}"
FIXTURE_LAB_BINGO_JPOT_SYNTH="${FIXTURE_LAB_BINGO_JPOT_SYNTH:-0}"
FIXTURE_LAB_BINGO_JPOT_GAME="${FIXTURE_LAB_BINGO_JPOT_GAME:-9206}"
FIXTURE_LAB_BINGO_JPOT_AMOUNT="${FIXTURE_LAB_BINGO_JPOT_AMOUNT:-}"
FIXTURE_LAB_BINGO_JPOT_TID="${FIXTURE_LAB_BINGO_JPOT_TID:-}"
FIXTURE_LAB_TRIPLE7S_JPOT_SYNTH="${FIXTURE_LAB_TRIPLE7S_JPOT_SYNTH:-0}"
FIXTURE_LAB_TRIPLE7S_JPOT_GAME="${FIXTURE_LAB_TRIPLE7S_JPOT_GAME:-970C}"
FIXTURE_LAB_TRIPLE7S_JPOT_TID="${FIXTURE_LAB_TRIPLE7S_JPOT_TID:-}"
# Prefer FIXTURE_*; accept LAB_* aliases from changelog / quick lab export.
FIXTURE_LAB_WITHHOLD_STATUS_0D="${FIXTURE_LAB_WITHHOLD_STATUS_0D:-${LAB_WITHHOLD_STATUS_0D:-0}}"
FIXTURE_LAB_WITHHOLD_STATUS_0D_COUNT="${FIXTURE_LAB_WITHHOLD_STATUS_0D_COUNT:-${LAB_WITHHOLD_STATUS_0D_COUNT:-1}}"
LAB_GAMEPLAY_SYNTH="${LAB_GAMEPLAY_SYNTH:-0}"
LAB_GAMEPLAY_TID="${LAB_GAMEPLAY_TID:-}"
LAB_GAMEPLAY_GAMES="${LAB_GAMEPLAY_GAMES:-touch6,superpick,bingo,poker}"
LAB_GAMEPLAY_SESSION_S="${LAB_GAMEPLAY_SESSION_S:-300}"
LAB_GAMEPLAY_PERIOD_S="${LAB_GAMEPLAY_PERIOD_S:-3}"
LAB_GAMEPLAY_JITTER_S="${LAB_GAMEPLAY_JITTER_S:-0.5}"
LAB_GAMEPLAY_PHASE_S="${LAB_GAMEPLAY_PHASE_S:-}"
LAB_GAMEPLAY_SHUFFLE="${LAB_GAMEPLAY_SHUFFLE:-0}"
LAB_GAMEPLAY_SYNTH_ALLOW_PROD="${LAB_GAMEPLAY_SYNTH_ALLOW_PROD:-0}"
LAB_GAMEPLAY_TOUCH6_REPLICATE="${LAB_GAMEPLAY_TOUCH6_REPLICATE:-1}"
FIXTURE_EARLY_WIN_PHASE1="${FIXTURE_EARLY_WIN_PHASE1:-0}"
FIXTURE_EARLY_WIN_PHASE2_SLOTS="${FIXTURE_EARLY_WIN_PHASE2_SLOTS:-0}"
FIXTURE_EARLY_WIN_PHASE2_KENO="${FIXTURE_EARLY_WIN_PHASE2_KENO:-0}"
FIXTURE_FIREWORKS_WIN_THRESHOLD_USD="${FIXTURE_FIREWORKS_WIN_THRESHOLD_USD:-}"
FIXTURE_EARLY_WIN_DRY_RUN="${FIXTURE_EARLY_WIN_DRY_RUN:-0}"
FIXTURE_SCR_DIAG="${FIXTURE_SCR_DIAG:-0}"
FIXTURE_SCR_MASTER_NATIVE_ONLY="${FIXTURE_SCR_MASTER_NATIVE_ONLY:-0}"
FIXTURE_HS_WITNESS="${FIXTURE_HS_WITNESS:-1}"
FIXTURE_LINK_WATCH="${FIXTURE_LINK_WATCH:-1}"
LINK_WATCH_INTERVAL="${LINK_WATCH_INTERVAL:-1.0}"
LINK_RECOVER="${LINK_RECOVER:-0}"
LINK_RECOVER_AFTER="${LINK_RECOVER_AFTER:-0}"
LINK_RECOVER_COOLDOWN="${LINK_RECOVER_COOLDOWN:-15}"
LINK_RECOVER_HARD_AFTER="${LINK_RECOVER_HARD_AFTER:-0}"
POG422_UARTD="${POG422_UARTD:-1}"
POG422_UARTD_INJECT="${POG422_UARTD_INJECT:-1}"
FIXTURE_ETH_RELAY_ONLY="${FIXTURE_ETH_RELAY_ONLY:-0}"
FIXTURE_ETH_LIVE_PORTS_ONLY="${FIXTURE_ETH_LIVE_PORTS_ONLY:-1}"
FIXTURE_UART_ETH_EGRESS="${FIXTURE_UART_ETH_EGRESS:-}"
if [[ -z "${FIXTURE_LINK_ARP:-}" ]]; then
  FIXTURE_LINK_ARP=0
fi
if [[ "${LINK_RECOVER}" == "1" && "${LINK_RECOVER_AFTER}" == "0" ]]; then
  LINK_RECOVER_AFTER=5
fi

# EG 2.2.1.12: pre_tx=0; hybrid DE = min(TEMT, air+POST_AIR_MARGIN).
if [[ "${POG422_UARTD_INJECT:-0}" == "1" ]]; then
  PRE_TX_US="${PRE_TX_US:-0}"
  POST_TX_US="${POST_TX_US:-0}"
  POST_AIR_MARGIN_US="${POST_AIR_MARGIN_US:-400}"
  POST_HS_INACTIVE_US="${POST_HS_INACTIVE_US:-100}"
  HS_INACTIVE_WAIT_MS="${HS_INACTIVE_WAIT_MS:-80}"
  LOOP_SLEEP_US="${LOOP_SLEEP_US:-100}"
elif [[ "${FIXTURE_POS}" == "4" ]]; then
  PRE_TX_US="${PRE_TX_US:-0}"
  POST_TX_US="${POST_TX_US:-0}"
  POST_AIR_MARGIN_US="${POST_AIR_MARGIN_US:-400}"
  POST_HS_INACTIVE_US="${POST_HS_INACTIVE_US:-150}"
  HS_INACTIVE_WAIT_MS="${HS_INACTIVE_WAIT_MS:-80}"
  LOOP_SLEEP_US="${LOOP_SLEEP_US:-100}"
else
  PRE_TX_US="${PRE_TX_US:-0}"
  POST_TX_US="${POST_TX_US:-0}"
  POST_AIR_MARGIN_US="${POST_AIR_MARGIN_US:-400}"
  POST_HS_INACTIVE_US="${POST_HS_INACTIVE_US:-100}"
  HS_INACTIVE_WAIT_MS="${HS_INACTIVE_WAIT_MS:-80}"
  LOOP_SLEEP_US="${LOOP_SLEEP_US:-100}"
fi

if [[ "${FIXTURE_PROD:-0}" == "1" ]]; then
  STATS_INTERVAL="${STATS_INTERVAL:-0}"
else
  STATS_INTERVAL="${STATS_INTERVAL:-30}"
fi

if [[ -n "${FIXTURE_DIR}" ]]; then
  OUTPUT_DIR="${FIXTURE_DIR}/data"
  LOG_DIR="${FIXTURE_DIR}/logs"
fi

mkdir -p "${LOG_DIR}" "${OUTPUT_DIR}"

if [[ "${CLEAR_CAPTURE}" == "1" ]]; then
  pog422_wipe_capture_dirs "${OUTPUT_DIR}" "${LOG_DIR}"
  echo "Wiped capture dirs ${OUTPUT_DIR} and ${LOG_DIR}" >&2
fi

cleanup_uartd() {
  pog422_stop_uartd
}
trap cleanup_uartd EXIT INT TERM

if [[ "${POG422_UARTD}" != "1" || "${POG422_UARTD_INJECT}" != "1" ]]; then
  echo "FATAL: EG 2.2 requires POG422_UARTD=1 and POG422_UARTD_INJECT=1 (C owns UART)" >&2
  exit 1
fi

if ! pog422_start_uartd "${TIMING_ROOT}" "${FIXTURE_POS}" "${CHANNEL}" "${LOG_DIR}"; then
  exit 1
fi

if ! ls /dev/ttyXRUSB* >/dev/null 2>&1; then
  echo "FATAL: /dev/ttyXRUSB* not found" >&2
  exit 1
fi

export PYTHONUNBUFFERED=1
ARGS=(
  "${ROOT}/pog422_bridge.py"
  --fixture-pos "${FIXTURE_POS}"
  --pi-label "${PI_LABEL}"
  --channel "${CHANNEL}"
  --output-dir "${OUTPUT_DIR}"
  --pre-tx-us "${PRE_TX_US}"
  --post-tx-us "${POST_TX_US}"
  --post-air-margin-us "${POST_AIR_MARGIN_US}"
  --post-hs-inactive-us "${POST_HS_INACTIVE_US}"
  --hs-inactive-wait-ms "${HS_INACTIVE_WAIT_MS}"
  --loop-sleep-us "${LOOP_SLEEP_US}"
  --stats-interval "${STATS_INTERVAL}"
)

if [[ "${JUN11_ETH_LEGACY}" == "1" ]]; then
  ARGS+=(--interface "${INTERFACE}")
  BRIDGE_IF_DESC="${INTERFACE} (legacy br0)"
else
  ARGS+=(--eth-ports "${ETH_PORTS}")
  BRIDGE_IF_DESC="${ETH_PORTS} directional"
fi

if [[ "${FLUSH_RX_BEFORE_TX}" == "1" ]]; then
  ARGS+=(--flush-rx-before-tx)
fi
if [[ "${WAIT_HS_INACTIVE}" == "0" ]]; then
  ARGS+=(--no-wait-hs-inactive)
fi
if [[ "${VERBOSE}" == "1" ]]; then
  ARGS+=(--echo-events)
fi
# Shell clears when CLEAR_CAPTURE=1; bridge must append so it does not unlink
# events.jsonl while fixture_link_watch.py (started below) is already writing.
ARGS+=(--append-capture)
if [[ "${FIXTURE_PROD:-0}" == "1" ]]; then
  if [[ "${FIXTURE_LAB_KENO_JPOT_SYNTH}" == "1" \
     || "${FIXTURE_LAB_POKER_ROYAL_SYNTH}" == "1" \
     || "${FIXTURE_LAB_SPINJACK_FLUSHJACK_SYNTH}" == "1" \
     || "${FIXTURE_LAB_TOUCH6_JPOT_SYNTH}" == "1" \
     || "${FIXTURE_LAB_SUPERPICK_JPOT_SYNTH}" == "1" \
     || "${FIXTURE_LAB_BINGO_JPOT_SYNTH}" == "1" \
     || "${FIXTURE_LAB_TRIPLE7S_JPOT_SYNTH}" == "1" \
     || "${FIXTURE_LAB_WITHHOLD_STATUS_0D}" == "1" \
     || ( "${LAB_GAMEPLAY_SYNTH}" == "1" && "${LAB_GAMEPLAY_SYNTH_ALLOW_PROD}" != "1" ) ]]; then
    echo "FATAL: FIXTURE_PROD=1 cannot run with lab outcome/gameplay synth / withhold-0d enabled" >&2
    echo "  Clear FIXTURE_LAB_* / LAB_GAMEPLAY_SYNTH or run enable_lab_*_env.sh --off" >&2
    exit 1
  fi
  ARGS+=(--prod)
elif [[ "${FIXTURE_DEV_LIGHT:-0}" == "1" || "${FIXTURE_LOG_MODE:-}" == "dev_light" || "${FIXTURE_LOG_MODE:-}" == "dev-light" ]]; then
  ARGS+=(--dev-light)
fi
if [[ "${SUPPRESS_UART_ECHO}" == "1" ]]; then
  ARGS+=(--suppress-uart-echo)
fi
if [[ "${FIXTURE_STATUS_0D_FIRST_ONLY}" == "1" ]]; then
  ARGS+=(--status-0d-first-only)
fi
if [[ "${CHANNEL}" != "ois" ]]; then
if [[ "${FIXTURE_STATUS_0D_BLOCK_ALL}" == "1" ]]; then
  ARGS+=(--status-0d-block-all)
fi
if [[ "${FIXTURE_STATUS_0D_SYNTH}" == "1" ]]; then
  ARGS+=(--status-0d-synth)
fi
if [[ "${FIXTURE_STATUS_0D_FIRST_PASSTHROUGH}" == "1" ]]; then
  ARGS+=(--status-0d-first-passthrough)
fi
if [[ "${FIXTURE_STATUS_0D_SYNTH_FROM_X9}" == "1" ]]; then
  ARGS+=(--status-0d-synth-from-x9)
fi
if [[ "${FIXTURE_STATUS_0D_SYNTH_LOOSE_COUNTER}" == "1" ]]; then
  ARGS+=(--status-0d-synth-loose-counter)
fi
ARGS+=(--status-0d-dedupe-ms "${FIXTURE_STATUS_0D_DEDUPE_MS}")
if [[ "${FIXTURE_LAB_KENO_JPOT_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-keno-jpot-synth --lab-keno-jpot-game "${FIXTURE_LAB_KENO_JPOT_GAME}")
  if [[ -n "${FIXTURE_LAB_KENO_JPOT_AMOUNT}" ]]; then
    ARGS+=(--lab-keno-jpot-amount "${FIXTURE_LAB_KENO_JPOT_AMOUNT}")
  fi
  if [[ -n "${FIXTURE_LAB_KENO_JPOT_TID}" ]]; then
    ARGS+=(--lab-keno-jpot-tid "${FIXTURE_LAB_KENO_JPOT_TID}")
  fi
fi
if [[ "${FIXTURE_LAB_POKER_ROYAL_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-poker-royal-synth --lab-poker-game "${FIXTURE_LAB_POKER_GAME}")
  if [[ -n "${FIXTURE_LAB_POKER_JPOT_AMOUNT}" ]]; then
    ARGS+=(--lab-poker-jpot-amount "${FIXTURE_LAB_POKER_JPOT_AMOUNT}")
  fi
  if [[ -n "${FIXTURE_LAB_POKER_TID}" ]]; then
    ARGS+=(--lab-poker-tid "${FIXTURE_LAB_POKER_TID}")
  fi
fi
if [[ "${FIXTURE_LAB_SPINJACK_FLUSHJACK_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-spinjack-flushjack-synth --lab-spinjack-game "${FIXTURE_LAB_SPINJACK_GAME}")
  if [[ -n "${FIXTURE_LAB_SPINJACK_TID}" ]]; then
    ARGS+=(--lab-spinjack-tid "${FIXTURE_LAB_SPINJACK_TID}")
  fi
fi
if [[ "${FIXTURE_LAB_TOUCH6_JPOT_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-touch6-jpot-synth --lab-touch6-jpot-game "${FIXTURE_LAB_TOUCH6_JPOT_GAME}")
  if [[ -n "${FIXTURE_LAB_TOUCH6_JPOT_AMOUNT}" ]]; then
    ARGS+=(--lab-touch6-jpot-amount "${FIXTURE_LAB_TOUCH6_JPOT_AMOUNT}")
  fi
  if [[ -n "${FIXTURE_LAB_TOUCH6_JPOT_TID}" ]]; then
    ARGS+=(--lab-touch6-jpot-tid "${FIXTURE_LAB_TOUCH6_JPOT_TID}")
  fi
fi
if [[ "${FIXTURE_LAB_SUPERPICK_JPOT_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-superpick-jpot-synth --lab-superpick-jpot-game "${FIXTURE_LAB_SUPERPICK_JPOT_GAME}")
  if [[ -n "${FIXTURE_LAB_SUPERPICK_JPOT_AMOUNT}" ]]; then
    ARGS+=(--lab-superpick-jpot-amount "${FIXTURE_LAB_SUPERPICK_JPOT_AMOUNT}")
  fi
  if [[ -n "${FIXTURE_LAB_SUPERPICK_JPOT_TID}" ]]; then
    ARGS+=(--lab-superpick-jpot-tid "${FIXTURE_LAB_SUPERPICK_JPOT_TID}")
  fi
fi
if [[ "${FIXTURE_LAB_BINGO_JPOT_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-bingo-jpot-synth --lab-bingo-jpot-game "${FIXTURE_LAB_BINGO_JPOT_GAME}")
  if [[ -n "${FIXTURE_LAB_BINGO_JPOT_AMOUNT}" ]]; then
    ARGS+=(--lab-bingo-jpot-amount "${FIXTURE_LAB_BINGO_JPOT_AMOUNT}")
  fi
  if [[ -n "${FIXTURE_LAB_BINGO_JPOT_TID}" ]]; then
    ARGS+=(--lab-bingo-jpot-tid "${FIXTURE_LAB_BINGO_JPOT_TID}")
  fi
fi
if [[ "${FIXTURE_LAB_TRIPLE7S_JPOT_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-triple7s-jpot-synth --lab-triple7s-jpot-game "${FIXTURE_LAB_TRIPLE7S_JPOT_GAME}")
  if [[ -n "${FIXTURE_LAB_TRIPLE7S_JPOT_TID}" ]]; then
    ARGS+=(--lab-triple7s-jpot-tid "${FIXTURE_LAB_TRIPLE7S_JPOT_TID}")
  fi
fi
if [[ "${FIXTURE_LAB_WITHHOLD_STATUS_0D}" == "1" ]]; then
  ARGS+=(--lab-withhold-status-0d --lab-withhold-status-0d-count "${FIXTURE_LAB_WITHHOLD_STATUS_0D_COUNT}")
fi
if [[ "${LAB_GAMEPLAY_SYNTH}" == "1" ]]; then
  ARGS+=(--lab-gameplay-synth)
  if [[ -n "${LAB_GAMEPLAY_TID}" ]]; then
    ARGS+=(--lab-gameplay-tid "${LAB_GAMEPLAY_TID}")
  fi
  if [[ -n "${LAB_GAMEPLAY_GAMES}" ]]; then
    ARGS+=(--lab-gameplay-games "${LAB_GAMEPLAY_GAMES}")
  fi
  ARGS+=(--lab-gameplay-session-s "${LAB_GAMEPLAY_SESSION_S}")
  ARGS+=(--lab-gameplay-period-s "${LAB_GAMEPLAY_PERIOD_S}")
  ARGS+=(--lab-gameplay-jitter-s "${LAB_GAMEPLAY_JITTER_S}")
  if [[ -n "${LAB_GAMEPLAY_PHASE_S}" ]]; then
    ARGS+=(--lab-gameplay-phase-s "${LAB_GAMEPLAY_PHASE_S}")
  fi
  if [[ "${LAB_GAMEPLAY_SHUFFLE}" == "1" ]]; then
    ARGS+=(--lab-gameplay-shuffle)
  fi
  if [[ "${LAB_GAMEPLAY_SYNTH_ALLOW_PROD}" == "1" ]]; then
    ARGS+=(--lab-gameplay-synth-allow-prod)
  fi
fi
if [[ "${FIXTURE_EARLY_WIN_PHASE1}" == "1" ]]; then
  ARGS+=(--early-win-phase1)
fi
if [[ "${FIXTURE_EARLY_WIN_PHASE2_SLOTS}" == "1" ]]; then
  ARGS+=(--early-win-phase2-slots)
  if [[ -n "${FIXTURE_FIREWORKS_WIN_THRESHOLD_USD}" ]]; then
    ARGS+=(--fireworks-win-threshold-usd "${FIXTURE_FIREWORKS_WIN_THRESHOLD_USD}")
  fi
fi
if [[ "${FIXTURE_EARLY_WIN_PHASE2_KENO}" == "1" ]]; then
  ARGS+=(--early-win-phase2-keno)
  if [[ -n "${FIXTURE_FIREWORKS_WIN_THRESHOLD_USD}" ]]; then
    ARGS+=(--fireworks-win-threshold-usd "${FIXTURE_FIREWORKS_WIN_THRESHOLD_USD}")
  fi
fi
if [[ "${FIXTURE_EARLY_WIN_DRY_RUN}" == "1" ]]; then
  ARGS+=(--early-win-phase1-dry-run)
fi
fi
if [[ "${FIXTURE_SCR_DIAG}" == "1" ]]; then
  ARGS+=(--scr-diag)
fi
if [[ "${FIXTURE_SCR_MASTER_NATIVE_ONLY}" == "1" ]]; then
  ARGS+=(--scr-master-native-only)
fi
if [[ "${FIXTURE_HS_WITNESS}" == "0" ]]; then
  ARGS+=(--no-hs-witness)
fi
if [[ "${FIXTURE_ETH_RELAY_ONLY}" == "1" ]]; then
  ARGS+=(--eth-relay-only)
fi
if [[ -n "${FIXTURE_UART_ETH_EGRESS}" ]]; then
  ARGS+=(--uart-eth-egress "${FIXTURE_UART_ETH_EGRESS}")
elif [[ "${FIXTURE_ETH_LIVE_PORTS_ONLY}" != "1" ]]; then
  ARGS+=(--uart-to-all-eth-ports --eth-relay-all-ports)
fi
if [[ "${POG422_UARTD}" == "1" ]]; then
  ARGS+=(--uartd)
  if [[ "${POG422_UARTD_INJECT}" == "1" ]]; then
    ARGS+=(--uartd-inject)
  else
    ARGS+=(--no-uartd-inject)
  fi
else
  ARGS+=(--no-uartd)
fi

echo "Fixture bridge pos=${FIXTURE_POS} label=${PI_LABEL} eth=${BRIDGE_IF_DESC}" >&2
if [[ "${FIXTURE_PROD:-0}" == "1" ]]; then
  echo "Prod mode: ${OUTPUT_DIR}/errors.jsonl (errors-only, no packets.raw)" >&2
elif [[ "${FIXTURE_DEV_LIGHT:-0}" == "1" || "${FIXTURE_LOG_MODE:-}" == "dev_light" || "${FIXTURE_LOG_MODE:-}" == "dev-light" ]]; then
  echo "Dev-light: ${OUTPUT_DIR}/events.jsonl (faults/MRT/synth only, no packets.raw)" >&2
else
  echo "Events: ${OUTPUT_DIR}/events.jsonl" >&2
fi
echo "TX: pre=${PRE_TX_US}us post=${POST_TX_US}us air+${POST_AIR_MARGIN_US}us" >&2
echo "HS: wait_inactive=${WAIT_HS_INACTIVE} post_inactive=${POST_HS_INACTIVE_US}us max_wait=${HS_INACTIVE_WAIT_MS}ms" >&2
if [[ "${CHANNEL}" == "ois" ]]; then
  echo "OIS bridge: raw HS opaque forward — no status-0D / gameplay synth" >&2
elif [[ "${CHANNEL}" == "both" ]]; then
  echo "Dual bridge: machine status-0D synth + OIS raw HS — ETH tag byte M/O on 0x88B5 payload" >&2
  echo "Status 0x0D: first_only=${FIXTURE_STATUS_0D_FIRST_ONLY} block_all=${FIXTURE_STATUS_0D_BLOCK_ALL} synth=${FIXTURE_STATUS_0D_SYNTH} passthrough=${FIXTURE_STATUS_0D_FIRST_PASSTHROUGH} synth_from_x9=${FIXTURE_STATUS_0D_SYNTH_FROM_X9} loose_ctr=${FIXTURE_STATUS_0D_SYNTH_LOOSE_COUNTER} dedupe_ms=${FIXTURE_STATUS_0D_DEDUPE_MS}" >&2
else
  echo "Status 0x0D: first_only=${FIXTURE_STATUS_0D_FIRST_ONLY} block_all=${FIXTURE_STATUS_0D_BLOCK_ALL} synth=${FIXTURE_STATUS_0D_SYNTH} passthrough=${FIXTURE_STATUS_0D_FIRST_PASSTHROUGH} synth_from_x9=${FIXTURE_STATUS_0D_SYNTH_FROM_X9} loose_ctr=${FIXTURE_STATUS_0D_SYNTH_LOOSE_COUNTER} dedupe_ms=${FIXTURE_STATUS_0D_DEDUPE_MS}" >&2
  echo "LAB Keno jpot synth: enabled=${FIXTURE_LAB_KENO_JPOT_SYNTH} game=${FIXTURE_LAB_KENO_JPOT_GAME} amount=${FIXTURE_LAB_KENO_JPOT_AMOUNT:-auto} tid=${FIXTURE_LAB_KENO_JPOT_TID:-any}" >&2
  echo "LAB Poker royal synth: enabled=${FIXTURE_LAB_POKER_ROYAL_SYNTH} game=${FIXTURE_LAB_POKER_GAME} jpot_amount=${FIXTURE_LAB_POKER_JPOT_AMOUNT:-auto} tid=${FIXTURE_LAB_POKER_TID:-any}" >&2
  echo "LAB SpinJack flushjack synth: enabled=${FIXTURE_LAB_SPINJACK_FLUSHJACK_SYNTH} game=${FIXTURE_LAB_SPINJACK_GAME} tid=${FIXTURE_LAB_SPINJACK_TID:-any}" >&2
  echo "LAB Touch 6 jpot synth: enabled=${FIXTURE_LAB_TOUCH6_JPOT_SYNTH} game=${FIXTURE_LAB_TOUCH6_JPOT_GAME} amount=${FIXTURE_LAB_TOUCH6_JPOT_AMOUNT:-auto} tid=${FIXTURE_LAB_TOUCH6_JPOT_TID:-any}" >&2
  echo "LAB SuperPick jpot synth: enabled=${FIXTURE_LAB_SUPERPICK_JPOT_SYNTH} game=${FIXTURE_LAB_SUPERPICK_JPOT_GAME} amount=${FIXTURE_LAB_SUPERPICK_JPOT_AMOUNT:-auto} tid=${FIXTURE_LAB_SUPERPICK_JPOT_TID:-any}" >&2
  echo "LAB Bingo jpot synth: enabled=${FIXTURE_LAB_BINGO_JPOT_SYNTH} game=${FIXTURE_LAB_BINGO_JPOT_GAME} amount=${FIXTURE_LAB_BINGO_JPOT_AMOUNT:-748850} tid=${FIXTURE_LAB_BINGO_JPOT_TID:-any}" >&2
  echo "LAB Triple 7's jpot synth: enabled=${FIXTURE_LAB_TRIPLE7S_JPOT_SYNTH} game=${FIXTURE_LAB_TRIPLE7S_JPOT_GAME} tid=${FIXTURE_LAB_TRIPLE7S_JPOT_TID:-any}" >&2
  echo "LAB withhold status 0x0D: enabled=${FIXTURE_LAB_WITHHOLD_STATUS_0D} count=${FIXTURE_LAB_WITHHOLD_STATUS_0D_COUNT}" >&2
  echo "LAB gameplay synth: enabled=${LAB_GAMEPLAY_SYNTH} tid=${LAB_GAMEPLAY_TID:-none} games=${LAB_GAMEPLAY_GAMES} session_s=${LAB_GAMEPLAY_SESSION_S}" >&2
  if [[ "${FIXTURE_PROD:-0}" != "1" && "${FIXTURE_DEBUG_LOG_ROTATE:-1}" == "1" ]]; then
    echo "Debug log rotate: max_bytes=${FIXTURE_DEBUG_LOG_MAX_BYTES:-26214400} utc_daily=${FIXTURE_DEBUG_LOG_ROTATE_UTC_DAILY:-1} prune_age_s=${FIXTURE_DEBUG_LOG_MAX_AGE_S:-604800}" >&2
  fi
fi
echo "UART mode: uartd=${POG422_UARTD} inject=${POG422_UARTD_INJECT} socket=${POG422_UARTD_SOCK:-/tmp/pog422_uart_${FIXTURE_POS}.sock}" >&2

if [[ "${FIXTURE_LINK_WATCH}" == "1" ]]; then
  pkill -f "fixture_link_watch.py --fixture-pos ${FIXTURE_POS}" 2>/dev/null || true
  WATCH_ARGS=(
    "${ROOT}/fixture_link_watch.py"
    --fixture-pos "${FIXTURE_POS}"
    --pi-label "${PI_LABEL}"
    --output-dir "${OUTPUT_DIR}"
    --log-dir "${LOG_DIR}"
    --interval "${LINK_WATCH_INTERVAL}"
    --recover-cooldown "${LINK_RECOVER_COOLDOWN}"
    --recover-hard-after "${LINK_RECOVER_HARD_AFTER}"
  )
  if [[ "${LINK_RECOVER}" == "1" && "${LINK_RECOVER_AFTER}" -gt 0 ]]; then
    WATCH_ARGS+=(--recover-after "${LINK_RECOVER_AFTER}")
  else
    WATCH_ARGS+=(--recover-after 0)
  fi
  if [[ "${FIXTURE_LINK_ARP}" == "1" ]]; then
    WATCH_ARGS+=(--interface "${LINK_WATCH_INTERFACE}")
  else
    WATCH_ARGS+=(--no-arp)
  fi
  if [[ "${VERBOSE}" == "1" ]]; then
    WATCH_ARGS+=(--echo-events)
  fi
  if [[ "${FIXTURE_PROD:-0}" == "1" ]]; then
    WATCH_ARGS+=(--prod)
  fi
  if [[ "${FIXTURE_LINK_ARP}" == "1" ]]; then
    echo "Link watch: ARP probe via ${LINK_WATCH_INTERFACE} (interval=${LINK_WATCH_INTERVAL}s)" >&2
  else
    echo "Link watch: carrier/operstate on eth0+eth1 only (interval=${LINK_WATCH_INTERVAL}s)" >&2
  fi
  if [[ "${LINK_RECOVER}" == "1" && "${LINK_RECOVER_AFTER}" -gt 0 ]]; then
    echo "Link recover: soft after ${LINK_RECOVER_AFTER} fails, cooldown=${LINK_RECOVER_COOLDOWN}s" >&2
  fi
  if [[ "${LINK_RECOVER_HARD_AFTER}" -gt 0 ]]; then
    echo "Link recover: hard br0 reinit after ${LINK_RECOVER_HARD_AFTER} failed soft attempts" >&2
  fi
  python3 "${WATCH_ARGS[@]}" >> "${LOG_DIR}/fixture_link_watch.log" 2>&1 &
  LINK_WATCH_PID=$!
else
  LINK_WATCH_PID=""
fi

run_bridge_python() {
  if type ohd_eg_run >/dev/null 2>&1; then
    if [[ "${VERBOSE}" == "1" ]]; then
      ohd_eg_run python3 "${ARGS[@]}" 2>> "${LOG_DIR}/fixture_bridge.log"
    else
      ohd_eg_run python3 "${ARGS[@]}"
    fi
  else
    if [[ "${VERBOSE}" == "1" ]]; then
      python3 "${ARGS[@]}" 2>> "${LOG_DIR}/fixture_bridge.log"
    else
      python3 "${ARGS[@]}"
    fi
  fi
}

if [[ "${LINK_RECOVER_HARD_AFTER}" -gt 0 ]]; then
  while true; do
    run_bridge_python
    BRIDGE_EC=$?
    if [[ -f "${LOG_DIR}/.link_hard_recover" ]]; then
      rm -f "${LOG_DIR}/.link_hard_recover"
      if [[ -n "${LINK_WATCH_PID}" ]]; then
        kill "${LINK_WATCH_PID}" 2>/dev/null || true
        wait "${LINK_WATCH_PID}" 2>/dev/null || true
      fi
      echo "Hard link recover — restarting bridge without clearing capture" >&2
      if [[ "${JUN11_ETH_LEGACY}" == "1" ]]; then
        sudo FIXTURE_POS="${FIXTURE_POS}" INTERFACE="${INTERFACE}" LOG_DIR="${LOG_DIR}" \
          JUN11_ETH=br0 bash "${ROOT}/br0_link_recover.sh" hard >> "${LOG_DIR}/link_recover.log" 2>&1 || true
      else
        sudo FIXTURE_POS="${FIXTURE_POS}" DEVICE="${LINK_WATCH_INTERFACE}" LOG_DIR="${LOG_DIR}" \
          JUN11_ETH=directional bash "${ROOT}/br0_link_recover.sh" hard >> "${LOG_DIR}/link_recover.log" 2>&1 || true
      fi
      if [[ "${FIXTURE_LINK_WATCH}" == "1" ]]; then
        python3 "${WATCH_ARGS[@]}" >> "${LOG_DIR}/fixture_link_watch.log" 2>&1 &
        LINK_WATCH_PID=$!
      fi
      continue
    fi
    if [[ -n "${LINK_WATCH_PID}" ]]; then
      kill "${LINK_WATCH_PID}" 2>/dev/null || true
    fi
    exit "${BRIDGE_EC}"
  done
else
  if type ohd_eg_exec >/dev/null 2>&1; then
    if [[ "${VERBOSE}" == "1" ]]; then
      ohd_eg_exec python3 "${ARGS[@]}" 2>> "${LOG_DIR}/fixture_bridge.log"
    else
      ohd_eg_exec python3 "${ARGS[@]}"
    fi
  else
    if [[ "${VERBOSE}" == "1" ]]; then
      exec python3 "${ARGS[@]}" 2>> "${LOG_DIR}/fixture_bridge.log"
    else
      exec python3 "${ARGS[@]}"
    fi
  fi
fi
