#!/usr/bin/env bash
# start-watch.sh — harness-owned webpack watcher with dual-writer prevention
# scripts/extension/live.sh)
#
# Purpose:
#   Clean-build path for one extension checkout: clears the webpack cache,
#   starts (or reuses) the harness-owned `yarn start` watcher, and waits for
#   a compile success marker. Refuses to run if a slot-owned watcher is alive.
#
# Inputs (flags / env):
#   --target <metamask-extension> (default $PWD; becomes the working dir)
#   --runtime-dir <rel> (default from adapters/shared path-defaults)
#   --watcher-port <port> (default env WATCHER_PORT/RECIPE_WATCHER_PORT,
#     runtime json, or slot suffix e.g. metamask-extension-1 => 9011)
#   --runner-bin <path> (optional; records deps/cache baseline best-effort)
#   --summary <file> (optional; writes the standard summary.json shape)
#
# Outputs:
#   <runtime-dir>/recipe-harness-webpack.{pid,log}; optional --summary file
#   {feature,status,inputs,outputs,generatedAt}.
#   Exit 0 — compiled; 1 — slot watcher owns the checkout, build failed, or
#   compile-marker timeout (240x2s); 2 — bad args.
#
# Never touches: the slot-owned webpack.pid watcher process (refusal only,
# removes the pid file only when stale); product source files.
set -euo pipefail

TARGET="$PWD"
RUNTIME_DIR=""
WATCHER_PORT="${WATCHER_PORT:-${RECIPE_WATCHER_PORT:-}}"
RUNNER_BIN=""
SUMMARY=""
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
while [ "$#" -gt 0 ]; do
  case "$1" in
    --target) require_value "$@"; TARGET="$2"; shift 2 ;;
    --runtime-dir) require_value "$@"; RUNTIME_DIR="$2"; shift 2 ;;
    --watcher-port) require_value "$@"; WATCHER_PORT="$2"; shift 2 ;;
    --runner-bin) require_value "$@"; RUNNER_BIN="$2"; shift 2 ;;
    --summary) require_value "$@"; SUMMARY="$2"; shift 2 ;;
    -h|--help)
      echo "Usage: start-watch.sh [--target <metamask-extension>] [--runtime-dir <rel>] [--watcher-port <port>] [--runner-bin <path>] [--summary <file>]"
      exit 0
      ;;
    *) echo "Unknown arg: $1" >&2; exit 2 ;;
  esac
done

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
for _arn in "$SCRIPT_DIR/lib/activate-repo-node.sh" "$SCRIPT_DIR/../shared/activate-repo-node.sh"; do
  [ -f "$_arn" ] && { . "$_arn"; break; }
done
unset _arn
if [ -z "$RUNTIME_DIR" ]; then
  # shellcheck disable=SC1091
  for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
    [ -f "$_hp" ] && { . "$_hp"; break; }
  done
  unset _hp
  if ! command -v recipe_runtime_dir >/dev/null 2>&1; then
    echo "start-watch: shared lib adapters/shared/harness-path.sh not found; pass --runtime-dir." >&2
    exit 1
  fi
  RUNTIME_DIR="$(recipe_runtime_dir)"
fi

cd "$TARGET"
REPO_NODE_PATH_PREFIX=""
for _arn in "$SCRIPT_DIR/lib/activate-repo-node.sh" "$SCRIPT_DIR/../shared/activate-repo-node.sh"; do
  [ -f "$_arn" ] && { # shellcheck disable=SC1090
    . "$_arn"
    break
  }
done
unset _arn
if command -v require_repo_node >/dev/null 2>&1; then
  require_repo_node "$TARGET"
elif command -v activate_repo_node >/dev/null 2>&1; then
  activate_repo_node "$TARGET" || {
    echo "[recipe-harness] refusing to start yarn start without a pinned Node for $TARGET" >&2
    exit 1
  }
else
  echo "[recipe-harness] activate-repo-node.sh missing from harness; run: mm-harness install" >&2
  exit 1
fi
if [ -z "$WATCHER_PORT" ]; then
  WATCHER_PORT="$(node - "$PWD" "$RUNTIME_DIR" <<'NODE'
const fs = require('fs');
const path = require('path');
const [target, runtimeDir] = process.argv.slice(2);
for (const name of ['recipe-runtime.json', 'agentic-runtime.json']) {
  const file = path.join(target, runtimeDir, name);
  if (!fs.existsSync(file)) continue;
  try {
    const data = JSON.parse(fs.readFileSync(file, 'utf8'));
    const value = data.watcherPort ?? data.devServerPort;
    if (value !== undefined && value !== null && value !== '') {
      process.stdout.write(String(value));
      process.exit(0);
    }
  } catch {
    process.exit(1);
  }
}
const match = path.basename(target).match(/-(\d+)$/u);
if (match) process.stdout.write(`901${match[1]}`);
NODE
)"
fi
if [ -n "$WATCHER_PORT" ] && ! [[ "$WATCHER_PORT" =~ ^[0-9]+$ ]]; then
  echo "start-watch: --watcher-port must be numeric (got: $WATCHER_PORT)" >&2
  exit 2
fi
export WATCHER_PORT RECIPE_WATCHER_PORT="$WATCHER_PORT"

status=fail
finish() {
  if [ -n "$SUMMARY" ]; then
    mkdir -p "$(dirname "$SUMMARY")"
    STATUS_FOR_SUMMARY="$status" TARGET_FOR_SUMMARY="$PWD" RUNTIME_DIR_FOR_SUMMARY="$RUNTIME_DIR" WATCHER_PORT_FOR_SUMMARY="$WATCHER_PORT" SUMMARY_PATH="$SUMMARY" node <<'NODE' || true
const fs = require('fs');
fs.writeFileSync(process.env.SUMMARY_PATH, `${JSON.stringify({
  feature: 'extension/start-watch',
  status: process.env.STATUS_FOR_SUMMARY,
  inputs: {
    target: process.env.TARGET_FOR_SUMMARY,
    runtimeDir: process.env.RUNTIME_DIR_FOR_SUMMARY,
    watcherPort: process.env.WATCHER_PORT_FOR_SUMMARY ? Number(process.env.WATCHER_PORT_FOR_SUMMARY) : null,
  },
  outputs: {
    watchPidFile: `${process.env.RUNTIME_DIR_FOR_SUMMARY}/recipe-harness-webpack.pid`,
    watchLog: `${process.env.RUNTIME_DIR_FOR_SUMMARY}/recipe-harness-webpack.log`,
  },
  generatedAt: new Date().toISOString(),
}, null, 2)}\n`);
NODE
  fi
}
trap finish EXIT

mkdir -p "$RUNTIME_DIR"

PRODUCT_CONFIG_MODULE="$SCRIPT_DIR/lib/product-config.cjs" \
  node - "$PWD" "$RUNTIME_DIR/extension-product-config.sha256" <<'NODE'
const crypto = require('node:crypto');
const fs = require('node:fs');
const config = require(process.env.PRODUCT_CONFIG_MODULE);
const [target, destination] = process.argv.slice(2);
const resolution = config.resolveExtensionInfuraProjectId(target, process.env);
if (resolution.kind !== 'configured') {
  fs.rmSync(destination, { force: true });
  process.exit(0);
}
const fingerprint = crypto.createHash('sha256').update(resolution.value).digest('hex');
fs.writeFileSync(destination, `${fingerprint}\n`);
NODE

watcher_pids() {
  ps -axo pid=,command= | while read -r current_pid command; do
    [ -n "${current_pid:-}" ] || continue
    case "$command" in
      *"yarn start"*|*"webpack --watch"*|*"development/webpack/launch.ts --watch"*) ;;
      *) continue ;;
    esac
    if printf '%s' "$command" | grep -Fq "$PWD"; then
      printf '%s\n' "$current_pid"
      continue
    fi
    if command -v lsof >/dev/null 2>&1; then
      if lsof -a -p "$current_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | grep -Fxq "$PWD"; then
        printf '%s\n' "$current_pid"
      fi
    fi
  done | sed '/^$/d'
  true
}

write_canonical_watch_pid() {
  local live_pids live_pid
  live_pids="$(watcher_pids || true)"
  if [ -z "$live_pids" ] && [ -n "${started_pid:-}" ] && kill -0 "$started_pid" >/dev/null 2>&1; then
    live_pids="$started_pid"
  fi
  [ -n "$live_pids" ] || return 1
  live_pid="$(printf '%s\n' "$live_pids" | head -1)"
  printf '%s\n' "$live_pid" > "$watch_pid_file"
  printf '%s\n' "$live_pid" > "$slot_watch_pid_file"
}

# Explicit clean-build path: clear the target-scoped webpack cache before
# starting the harness-owned watcher. Faster incremental refresh stays on
# `mm-harness refresh`; this script is for proving a clean build works.
echo '[recipe-harness] clean build: clearing webpack cache + harness watcher'
rm -rf node_modules/.cache/webpack
killed_harness_pid=""
if [ -f "$RUNTIME_DIR/recipe-harness-webpack.pid" ]; then
  killed_harness_pid="$(cat "$RUNTIME_DIR/recipe-harness-webpack.pid" 2>/dev/null || true)"
  [ -z "$killed_harness_pid" ] || kill -- "-$killed_harness_pid" >/dev/null 2>&1 || true
  [ -z "$killed_harness_pid" ] || kill "$killed_harness_pid" >/dev/null 2>&1 || true
  rm -f "$RUNTIME_DIR/recipe-harness-webpack.pid"
fi
if [ -f "$RUNTIME_DIR/recipe-harness-webpack-sync.pid" ]; then
  sync_pid="$(cat "$RUNTIME_DIR/recipe-harness-webpack-sync.pid" 2>/dev/null || true)"
  [ -z "$sync_pid" ] || kill "$sync_pid" >/dev/null 2>&1 || true
  rm -f "$RUNTIME_DIR/recipe-harness-webpack-sync.pid"
fi

# Do not start a second writer for dist/chrome. Slot-owned watches use
# webpack.pid; harness-owned watches use recipe-harness-webpack.pid.
slot_watch_pid_file="$RUNTIME_DIR/webpack.pid"
if [ -f "$slot_watch_pid_file" ]; then
  slot_watch_pid=$(cat "$slot_watch_pid_file" 2>/dev/null || true)
else
  slot_watch_pid=
fi
if [ -n "$slot_watch_pid" ] && [ "$slot_watch_pid" = "$killed_harness_pid" ]; then
  rm -f "$slot_watch_pid_file"
  slot_watch_pid=
fi
if [ -n "$slot_watch_pid" ] && kill -0 "$slot_watch_pid" >/dev/null 2>&1; then
  echo "[recipe-harness] Refusing to start yarn start: slot webpack watch already owns this checkout (pid $slot_watch_pid). Stop it first or reuse the live browser." >&2
  exit 1
fi
[ -z "$slot_watch_pid" ] || rm -f "$slot_watch_pid_file"

existing_watch_pids="$(watcher_pids || true)"
if [ -n "$existing_watch_pids" ]; then
  echo "[recipe-harness] Stopping stale target-scoped watcher(s): $(printf '%s' "$existing_watch_pids" | tr '\n' ' ')"
  printf '%s\n' "$existing_watch_pids" | xargs kill 2>/dev/null || true
  sleep 2
  existing_watch_pids="$(watcher_pids || true)"
  if [ -n "$existing_watch_pids" ]; then
    printf '%s\n' "$existing_watch_pids" | xargs kill -9 2>/dev/null || true
  fi
fi

# Scope watcher reuse to this checkout. A machine-global pgrep can match an
# unrelated repo and leave this target validating stale dist/chrome output.
watch_pid_file="$RUNTIME_DIR/recipe-harness-webpack.pid"
watch_log="$RUNTIME_DIR/recipe-harness-webpack.log"
sync_pid_file="$RUNTIME_DIR/recipe-harness-webpack-sync.pid"
sync_log="$RUNTIME_DIR/recipe-harness-webpack-sync.log"
rm -f "$watch_pid_file"
: > "$watch_log"
LIB_DIR=""
for _lib in "$SCRIPT_DIR/lib" "$SCRIPT_DIR/../shared"; do
  [ -f "$_lib/log-tui.mjs" ] && LIB_DIR="$_lib" && break
done
unset _lib
echo "[recipe-harness] Starting yarn start (watcher-port=${WATCHER_PORT:-default})"
echo "[recipe-harness] compact build view - full log: $watch_log (mm-harness logs --full)"
started_pid=""
path_prefix="${REPO_NODE_PATH_PREFIX:-}"
launcher="$SCRIPT_DIR/launch-webpack.cjs"
[ -f "$launcher" ] || { echo "launch-webpack.cjs missing from harness; run: mm-harness install" >&2; exit 1; }
sync_script="$SCRIPT_DIR/sync-webpack-dist.cjs"
[ -f "$sync_script" ] || { echo "sync-webpack-dist.cjs missing from harness; run: mm-harness install" >&2; exit 1; }
launch_args=(--target "$PWD" --log "$watch_log" --pid-file "$watch_pid_file" --sync-script "$sync_script" --sync-pid-file "$sync_pid_file" --sync-log "$sync_log" --dist "$PWD/dist/chrome" --runtime-dist "$PWD/$RUNTIME_DIR/${RECIPE_RUNTIME_DIST_DIR:-runtime-dist}")
[ -z "$WATCHER_PORT" ] || launch_args+=(--port "$WATCHER_PORT")
[ -z "$path_prefix" ] || launch_args+=(--path-prefix "$path_prefix")
env -u BUNDLED_DEBUGPY_PATH node "$launcher" "${launch_args[@]}" >/dev/null
started_pid="$(cat "$watch_pid_file")"
# The orchestrator names the session; the harness only populates windows. Resolve
# via the run-owned ladder (RECIPE_TMUX_SESSION → agentic-runtime.json session) —
# no current-session fallback or slot-number derivation.
for _tv_src in "$SCRIPT_DIR/lib/tmux-viewer.sh" "$SCRIPT_DIR/../shared/tmux-viewer.sh"; do
  # shellcheck disable=SC1091
  [ -f "$_tv_src" ] && { . "$_tv_src"; break; }
done
unset _tv_src
if command -v tmux_viewer_open >/dev/null 2>&1; then
  log_q="$(printf '%q' "$watch_log")"
  tmux_viewer_open "webpack" "${WATCHER_PORT:-default}" "$watch_log" "$RUNTIME_DIR" "$RUNTIME_DIR/webpack.tmux" "exec tail -n +1 -F $log_q"
fi
ln -sf "$(basename "$watch_log")" "$RUNTIME_DIR/webpack.log"

watch_tail_pid=""
if [ "${RECIPE_LOG_UI:-compact}" = "full" ]; then
  tail -n +1 -F "$watch_log" &
  watch_tail_pid=$!
elif [ -n "$LIB_DIR" ] && [ "${RECIPE_LOG_TUI_PARENT:-}" != "1" ]; then
  node "$LIB_DIR/log-tui.mjs" watch \
    --log "$watch_log" \
    --events "${RECIPE_LOG_EVENTS:-10}" \
    --mode "${RECIPE_LOG_UI:-compact}" \
    --label "webpack build" \
    --success-pattern 'compiled successfully|compiled with [0-9]+ warning|MetaMask .* compiled|Bundle end: service worker|Bundle end:.*app-init' &
  watch_tail_pid=$!
fi
compiled=false
for i in {1..240}; do
  if [ -n "$watch_tail_pid" ] && ! kill -0 "$watch_tail_pid" >/dev/null 2>&1; then
    wait "$watch_tail_pid" 2>/dev/null || true
    if grep -Eq 'compiled successfully|compiled with [0-9]+ warning|MetaMask .* compiled|Bundle end: service worker|Bundle end:.*app-init' "$watch_log" 2>/dev/null; then
      compiled=true
      break
    fi
    echo '[recipe-harness] compact build watcher exited before compile marker; continuing to poll webpack log' >&2
    watch_tail_pid=""
  fi
  if ! write_canonical_watch_pid; then
    if [ "$i" -le 10 ]; then
      sleep 2
      continue
    fi
    [ -n "$watch_tail_pid" ] && kill "$watch_tail_pid" >/dev/null 2>&1 || true
    [ -n "$watch_tail_pid" ] && wait "$watch_tail_pid" 2>/dev/null || true
    echo '[recipe-harness] yarn start exited before webpack emitted a compile marker:' >&2
    if [ -n "$LIB_DIR" ]; then
      node "$LIB_DIR/log-tui.mjs" events --log "$watch_log" --events 15 >&2 || true
    else
      tail -40 "$watch_log" >&2 || true
    fi
    rm -f "$watch_pid_file" "$slot_watch_pid_file"
    exit 1
  fi
  if grep -Eq 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error' "$watch_log" 2>/dev/null; then
    [ -n "$watch_tail_pid" ] && kill "$watch_tail_pid" >/dev/null 2>&1 || true
    [ -n "$watch_tail_pid" ] && wait "$watch_tail_pid" 2>/dev/null || true
    echo '[recipe-harness] webpack BUILD FAILED (not waiting for timeout):' >&2
    [ -n "$LIB_DIR" ] && node "$LIB_DIR/log-tui.mjs" events --log "$watch_log" --events 15 >&2 || true
    echo '[recipe-harness] full log:' "$watch_log" >&2
    echo '[recipe-harness] If it is a stale-cache ENOENT it should have been auto-cleared; a recurring error here is a real source/build issue to fix.' >&2
    exit 1
  fi
  if grep -Eq 'compiled successfully|compiled with [0-9]+ warning|MetaMask .* compiled|Bundle end: service worker|Bundle end:.*app-init' "$watch_log" 2>/dev/null; then
    compiled=true
    [ -n "$watch_tail_pid" ] && kill "$watch_tail_pid" >/dev/null 2>&1 || true
    [ -n "$watch_tail_pid" ] && wait "$watch_tail_pid" 2>/dev/null || true
    break
  fi
  sleep 2
done
if [ "$compiled" != true ]; then
  [ -n "$watch_tail_pid" ] && kill "$watch_tail_pid" >/dev/null 2>&1 || true
  [ -n "$watch_tail_pid" ] && wait "$watch_tail_pid" 2>/dev/null || true
  echo 'Timed out waiting for target-scoped yarn start compilation marker' >&2
  if [ -n "$LIB_DIR" ]; then
    node "$LIB_DIR/log-tui.mjs" events --log "$watch_log" --events 15 >&2 || true
  else
    tail -40 "$watch_log" >&2 || true
  fi
  exit 1
fi
if ! write_canonical_watch_pid; then
  echo '[recipe-harness] webpack emitted a compile marker, but no live target-scoped watcher remained:' >&2
  tail -80 "$watch_log" >&2 || true
  rm -f "$watch_pid_file" "$slot_watch_pid_file"
  exit 1
fi
echo '[recipe-harness] yarn start compiled successfully'
if [ -n "$RUNNER_BIN" ]; then
  "$RUNNER_BIN" runtime-decision --adapter extension --target . --record-baseline --json >/dev/null 2>&1 || true
  echo '[recipe-harness] recorded webpack cache baseline (runtime-decision --record-baseline)'
fi
status=pass
