#!/usr/bin/env bash
# Spawn the eight log streams that make up the dev observability surface,
# tee Metro stdout, and (after streams are attached) launch the iOS app
# with `--console-pty` so startup crashes are captured. Writes a `.pids`
# file so `stop-logs.sh` can clean up.
#
# Foundation doc cross-refs: §3.2.4 (two predicate strategies),
# §3.2.5 (JS errors through the system log), §3.2.6 (pre-launch attach),
# §3.2.10 (Metro tee for console.log reliability).

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
source "$SCRIPT_DIR/sim.env"

cd "$REPO_ROOT"

SESSION_DIR="temp/dev-logs/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$SESSION_DIR/responses"
PIDS="$SESSION_DIR/.pids"
: > "$PIDS"

note() {
  echo "[tail-logs] $*"
}

note "Session: $SESSION_DIR"

# ---- Port 8081 must be free before we spawn Metro. If a stale Expo
# from an earlier session still owns the port, `npx expo start` sees
# it, asks "Use port 8082 instead?" on stdin, gets EOF (we detach
# stdin below), and silently prints "Skipping dev server" — leaving
# metro.log empty and every awk demuxer (net/wc/auth/push) tailing a
# file that will never get RN console output. That failure mode took
# a whole session to spot, so refuse to proceed and point at the
# recovery command. ----
if lsof -ti :8081 >/dev/null 2>&1; then
  note "✗ Port 8081 already bound by:"
  lsof -i :8081 -sTCP:LISTEN -P 2>/dev/null | sed 's/^/    /' >&2 || true
  note "  Metro can't start here — RN devlog streams (net/wc/auth/push) would be empty."
  note "  Recovery: scripts/dev/stop-logs.sh   (or: lsof -ti :8081 | xargs kill -9)"
  exit 1
fi

# ---- Metro tee (RN console.log universe) ----
# Detach stdin (`< /dev/null`) so the Expo CLI doesn't see EOF and
# self-shutdown — backgrounding the script inherits the parent's
# stdin, which is `/dev/null` under most CI/tooling invocations and
# causes `expo start`'s interactive prompt loop to read EOF on first
# tick and print "Stopped server".
note "Starting Metro (Expo dev server)…"
( npx expo start --dev-client < /dev/null ) > "$SESSION_DIR/metro.log" 2>&1 &
echo $! >> "$PIDS"

# ---- iOS system log (process-filtered — catches NSLog, ATS, RN bridge
# `[com.facebook.react.log:javascript]`, exit signals) ----
note "Streaming iOS system log…"
xcrun simctl spawn "$IPHONE_UDID" log stream \
  --level=debug \
  --color=none \
  --style=compact \
  --predicate "process == \"$IOS_SCHEME\"" \
  > "$SESSION_DIR/ios-app-system.log" 2>&1 &
echo $! >> "$PIDS"

# ---- iOS subsystem log (intentional emits — empty until a native
# os.Logger bridge exists; preserved as a slot per the plan §5.2 Option A) ----
xcrun simctl spawn "$IPHONE_UDID" log stream \
  --level=debug \
  --color=none \
  --style=compact \
  --predicate "subsystem == \"$IOS_SUBSYSTEM\"" \
  > "$SESSION_DIR/ios-app.log" 2>&1 &
echo $! >> "$PIDS"

# ---- JS-error subset (carved out of ios-app-system) ----
xcrun simctl spawn "$IPHONE_UDID" log stream \
  --level=debug \
  --color=none \
  --style=compact \
  --predicate "process == \"$IOS_SCHEME\" && eventMessage CONTAINS \"com.facebook.react.log:javascript\"" \
  > "$SESSION_DIR/js-errors.log" 2>&1 &
echo $! >> "$PIDS"

# ---- Watch subsystem log (intentional emits via DebugRecorder bridge
# + the complication's getTimeline emit) ----
note "Streaming Watch subsystem log…"
xcrun simctl spawn "$WATCH_UDID" log stream \
  --level=debug \
  --color=none \
  --style=compact \
  --predicate "subsystem == \"$WATCH_SUBSYSTEM\"" \
  > "$SESSION_DIR/watch-app.log" 2>&1 &
echo $! >> "$PIDS"

# ---- Watch system log (process-filtered — framework chatter, NSLog,
# crash signals; complication runs as a chronoseed-hosted extension so
# include it as well) ----
xcrun simctl spawn "$WATCH_UDID" log stream \
  --level=debug \
  --color=none \
  --style=compact \
  --predicate 'process == "HalflightWatch" OR process == "HalflightWatchComplication" OR process == "chronoseed"' \
  > "$SESSION_DIR/watch-app-system.log" 2>&1 &
echo $! >> "$PIDS"

# ---- JS demultiplex — split Metro stdout by [net] / [wc] / [auth] /
# [push] tags into per-category files. `tail -F` (not `-f`) survives
# Metro restart; `grep --line-buffered` exits when the upstream `tail`
# exits, so kill tails on stop-logs to cascade-clean these.
#
# Two non-obvious things bit this earlier:
#   1. Expo CLI prefixes every RN console.log with ` LOG  ` (and
#      warn/error with ` WARN  ` / ` ERROR  `), so an `^\[net\]`
#      anchor matches zero lines. Match the bracketed tag anywhere.
#   2. The previous `awk '/\[net\]/'` form is correct *but* awk
#      block-buffers stdout when it's a regular file. Lines matched
#      but sat in awk's 4 KB buffer instead of reaching net.log, so
#      tooling that reads the file mid-session saw zero events.
#      grep --line-buffered is the macOS-portable fix; the BSD awk
#      shipped with macOS does not honor `-W interactive`. ----
( tail -F "$SESSION_DIR/metro.log" 2>/dev/null | grep --line-buffered '\[net\]' > "$SESSION_DIR/net.log" ) &
echo $! >> "$PIDS"
( tail -F "$SESSION_DIR/metro.log" 2>/dev/null | grep --line-buffered '\[wc\]' > "$SESSION_DIR/wc.log" ) &
echo $! >> "$PIDS"
( tail -F "$SESSION_DIR/metro.log" 2>/dev/null | grep --line-buffered '\[auth\]' > "$SESSION_DIR/auth.log" ) &
echo $! >> "$PIDS"
( tail -F "$SESSION_DIR/metro.log" 2>/dev/null | grep --line-buffered '\[push\]' > "$SESSION_DIR/push.log" ) &
echo $! >> "$PIDS"

# ---- Tail-of-tail chain (design constraint, read this before adding more) ----
# The four demuxers below tail `net.log`, which is itself a tail-of
# `metro.log` (the [net] demuxer up at line 123). One link of cascade
# already. Implications a future addition needs to honour:
#
#   1. If the upstream net.log demuxer dies, every downstream stream
#      goes silent at the same moment. `diagnose.sh` will surface
#      net.log as a STALE canary (it's in CANARY_LOGS) so the upstream
#      failure is visible — but the downstream streams DON'T fail
#      independently. Don't add a sentinel daemon or canary check on
#      tap-page/bucket/since/429: their liveness is net.log's liveness.
#   2. The orphan-sweep in stop-logs.sh has a `tail -F .*net\.log`
#      pattern for the same reason. New downstreams of net.log are
#      already covered by it; new downstreams of OTHER streams need
#      their own pkill pattern there.
#   3. Don't deepen the chain further. Tailing one of these tap-* files
#      to build a tap-of-tap-of-tap is two cascades and four silent
#      failure modes. If you need a finer cut (e.g. "tap-page failures
#      only"), grep net.log directly — the source is the same.
#
# ---- Tapestry net carve-outs — net.log is the firehose; on a busy
# session it carries bucket polls, marker writes, verify_credentials,
# image probes. A §6.5-shaped finding ("the eager backfill is walking
# 4000 pages") is invisible buried under those, so split the three
# tapestry endpoints into their own tiny streams. Each pre-greped file
# stays in the KB range vs net.log's MB so reading the tail is
# O(seconds), not O(minutes). ----
( tail -F "$SESSION_DIR/net.log" 2>/dev/null | grep --line-buffered 'tapestry/page/'   > "$SESSION_DIR/net-tap-page.log" ) &
echo $! >> "$PIDS"
( tail -F "$SESSION_DIR/net.log" 2>/dev/null | grep --line-buffered 'tapestry/bucket/' > "$SESSION_DIR/net-tap-bucket.log" ) &
echo $! >> "$PIDS"
( tail -F "$SESSION_DIR/net.log" 2>/dev/null | grep --line-buffered 'tapestry/since/'  > "$SESSION_DIR/net-tap-since.log" ) &
echo $! >> "$PIDS"

# ---- metro-hmr.log — edit-propagation visibility. fs.watch on src/
# emits a line per save; /events WebSocket emits bundle_build_done /
# bundling_error / hmr_client_error so a save that fails to compile
# lands a line right under the edit. Stays empty on a session with no
# code edits. ----
: > "$SESSION_DIR/metro-hmr.log"
( node "$SCRIPT_DIR/metro-hmr.mjs" "$SESSION_DIR/metro-hmr.log" >/dev/null 2>&1 ) &
echo $! >> "$PIDS"

# ---- 429.log — every rate-limited HTTP response in one place. Match
# the status field by anchoring on ` 429 ` (space-status-space-Nms);
# request IDs like `#429 GET …` start with `#`, not space, so they
# don't collide. Tiny but critical-when-non-zero; diagnose.sh's
# --human card surfaces the count only when > 0 (mirrors watch-img
# aggregator). ----
( tail -F "$SESSION_DIR/net.log" 2>/dev/null | grep --line-buffered ' 429 ' > "$SESSION_DIR/429.log" ) &
echo $! >> "$PIDS"

# ---- Watch image demuxer — split watch-app.log's `img` category into
# its own stream so transport / status / TLS failures aren't drowned
# in the watch's net / wc / ui / complication firehose. The compact-
# style log stream output renders each line as
# `... [<subsystem>:<category>] <msg>` — match the colon-prefixed
# category form, not bare `[img]`, since `[img]` doesn't appear as a
# substring of `[ht.halflig.app.watchkitapp:img]`. Pre-touch the
# output so `tail` reading mid-session doesn't ENOENT on a fresh
# install that hasn't yet had any watch emit. ----
: > "$SESSION_DIR/watch-img.log"
( tail -F "$SESSION_DIR/watch-app.log" 2>/dev/null | grep --line-buffered ':img\]' > "$SESSION_DIR/watch-img.log" ) &
echo $! >> "$PIDS"

# ---- Silence sentinels for the legitimately-quiet demuxer streams ----
# Periodically writes `--- HH:MM:SS silent for Ns ---` to any stream
# that hasn't grown in $SENTINEL_THRESHOLD seconds, so a reader can
# distinguish "healthy but quiet" from "demuxer broken / app silent".
# The sentinel itself counts as activity for size-tracking purposes,
# so at most one sentinel lands per threshold-window of pure silence.
#
# Writes go through `>>` (O_APPEND) so they don't race the demuxer's
# stdout-redirect: POSIX guarantees atomic appends for writes smaller
# than PIPE_BUF (≥ 4 KB on macOS); our sentinel line is ~30 bytes.
#
# net.log is deliberately excluded: it's the canary stream (the
# `generate_204` reachability probe fires every ~60s when the app is
# foregrounded). Empty net.log == demuxer broken, so a sentinel there
# would mask the very signal §3.2's canary probe is designed to surface.
SENTINEL_THRESHOLD=30
sentinel_stream() {
  local file="$1"
  local last_size=0
  local last_active_ts=$(date +%s)
  while true; do
    sleep 10
    [[ ! -f "$file" ]] && continue
    local cur_size now silent_s
    cur_size=$(stat -f "%z" "$file" 2>/dev/null || echo 0)
    now=$(date +%s)
    if [[ "$cur_size" -ne "$last_size" ]]; then
      last_size="$cur_size"
      last_active_ts="$now"
    fi
    silent_s=$(( now - last_active_ts ))
    if [[ "$silent_s" -ge "$SENTINEL_THRESHOLD" ]]; then
      printf '%s\n' "--- $(date '+%H:%M:%S') silent for ${silent_s}s ---" >> "$file"
      last_size=$(stat -f "%z" "$file" 2>/dev/null || echo "$cur_size")
      last_active_ts="$now"
    fi
  done
}
for stream in wc.log auth.log push.log; do
  sentinel_stream "$SESSION_DIR/$stream" &
  echo $! >> "$PIDS"
done

# ---- Wait for Metro to be ready before launching the app. Without
# this wait the app launches first, RCTBundleURLProvider fetches from
# localhost:8081, gets ECONNREFUSED, and the user sees the red
# "No script URL provided" screen. Probe `/status` directly — the
# simulator's `localhost` is the host machine's loopback, so the same
# URL the app will fetch is what we test from here.
#
# ~5-15s warm, ~30s cold. 60s ceiling.
note "Waiting for Metro to be ready…"
METRO_READY=0
for i in $(seq 1 60); do
  if curl -fsS -m 1 http://localhost:8081/status 2>/dev/null | grep -q 'packager-status:running'; then
    note "Metro ready (after ${i}s)."
    METRO_READY=1
    break
  fi
  sleep 1
done
if [[ "$METRO_READY" -ne 1 ]]; then
  note "⚠ Metro did not become ready within 60s — launching anyway; first frame will likely red-box."
fi

# ---- Launch the iOS app via plain `simctl launch`. This app does NOT
# ship `expo-dev-client` (no pod, no dev launcher screen), so the
# `halflight://expo-development-client/?url=…` deep link does nothing
# here. In DEBUG, AppDelegate's bundleURL() returns
# `RCTBundleURLProvider.sharedSettings().jsBundleURL(...)` which
# resolves to `http://localhost:8081/.expo/.virtual-metro-entry.bundle`
# — Metro on the host is reachable via the sim's localhost loopback.
# `--console` streams the iOS NSLog/print stream into ios-launch.log so
# startup crashes are captured even if our log streams missed them. ----
note "Launching iOS app ($IOS_BUNDLE_ID) on iPhone sim…"
xcrun simctl terminate "$IPHONE_UDID" "$IOS_BUNDLE_ID" 2>/dev/null || true
( xcrun simctl launch --console "$IPHONE_UDID" "$IOS_BUNDLE_ID" < /dev/null ) \
  > "$SESSION_DIR/ios-launch.log" 2>&1 &
echo $! >> "$PIDS"

# Symlink "latest" for quick `Read temp/dev-logs/latest/wc.log` access.
ln -sfn "$(basename "$SESSION_DIR")" temp/dev-logs/latest

note "All streams attached. Session: $SESSION_DIR"
note "Stop with: scripts/dev/stop-logs.sh"
echo
echo "Files being written under $SESSION_DIR:"
ls -1 "$SESSION_DIR" | sed 's/^/  /'

# ---- Demuxer canary: confirm a [net] line lands within 20s of app
# launch. Cold boot bundle-load is ~5-15s; the reachability probe
# (generate_204) is one of the first emits installNetLog produces, so
# this is the cleanest end-to-end check that (a) the app booted, (b)
# RN bundle loaded, (c) installNetLog ran, (d) the metro→grep demuxer
# pattern still matches. Three causes mapped in the warning below
# match the three documented failure modes in
# `APPLE_WATCH_DEBUG_NEXT_LEADS.md` §3.2. Runs in the background so
# the user gets their shell back immediately; warning appears inline. ----
(
  CANARY_DEADLINE=$((SECONDS + 20))
  while (( SECONDS < CANARY_DEADLINE )); do
    if [[ -s "$SESSION_DIR/net.log" ]]; then
      note "✓ Demuxer canary: [net] line captured ($(stat -f%z "$SESSION_DIR/net.log") bytes)."
      exit 0
    fi
    sleep 1
  done
  note "⚠ No [net] line in 20s — demuxers may not be capturing."
  note "  Causes: installNetLog() not running, grep pattern drifted, or app hasn't booted yet."
  note "  Re-run scripts/dev/diagnose.sh once the app has made a request."
) &
echo $! >> "$PIDS"

# Keep the script running so its child PIDs (group-leader scenario) stay
# parented to it. Wait until interrupted; stop-logs.sh kills via .pids.
wait
