#!/usr/bin/env node
// Edit-propagation visibility for the dev loop.
//
// Metro logs `iOS Bundled 48ms ... (1 module)` on HMR but omits the
// module name, so you can't confirm "my edit to src/lib/tapestry/range.ts
// is in the running bundle" without force-relaunching. This script
// closes the gap with two cheap sources:
//
//   1. fs.watch over src/ — every Save event becomes a line:
//        `[hmr 14:23:01] edit  src/lib/tapestry/range.ts`
//      Confirms the file you THINK you edited is the file you DID edit
//      (cmd-S into the wrong window happens). Bare timestamp = save
//      hit disk; the seconds-resolution stamp lines up with the bundle
//      times Metro prints on the next rebuild.
//
//   2. WebSocket subscription to ws://localhost:8081/events — Metro's
//      broadcast endpoint for build / HMR / error events. We log:
//        `[hmr] bundle_build_done` / `bundling_error: …` / `hmr_client_error: …`
//      so a save that fails to compile lands a line right under the
//      edit line. /events also surfaces app-side `client_log` events;
//      those flow to metro.log already so we drop them here.
//
// Invoked by tail-logs.sh; writes to <session>/metro-hmr.log. Exits
// cleanly on SIGTERM/SIGINT so stop-logs.sh can sweep it.

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import WebSocket from 'ws'

const __filename = fileURLToPath(import.meta.url)
const REPO_ROOT = path.resolve(path.dirname(__filename), '..', '..')
const OUT_PATH = process.argv[2]
if (!OUT_PATH) {
  console.error('usage: metro-hmr.mjs <output-log-path>')
  process.exit(2)
}

const WATCH_ROOT = path.join(REPO_ROOT, 'src')
const METRO_EVENTS_URL = 'ws://localhost:8081/events'

const out = fs.createWriteStream(OUT_PATH, { flags: 'a' })
const stamp = () => new Date().toTimeString().slice(0, 8)
const write = (line) => out.write(`[hmr ${stamp()}] ${line}\n`)

// ---- fs.watch on src/ ----
// fs.watch with recursive: true is supported on macOS and Linux 22+.
// It coalesces edits within ~10 ms; debounce per-path so a 'change'
// event burst (some editors save → temp → rename) lands as one line.
const recent = new Map()
const COALESCE_MS = 100

function onChange(eventType, relPath) {
  // eventType is 'rename' or 'change'. Both are interesting; the
  // visible signal is "this path was touched". Filter out junk paths
  // editors create on save (.tmp, .swp, ~) so they don't drown the
  // real edit line.
  if (!relPath) return
  if (/\.swp$/.test(relPath) || /~$/.test(relPath) || /\.tmp$/.test(relPath)) return
  const last = recent.get(relPath) ?? 0
  const now = Date.now()
  if (now - last < COALESCE_MS) return
  recent.set(relPath, now)
  const display = path.join('src', relPath)
  write(`edit  ${display}`)
}

try {
  fs.watch(WATCH_ROOT, { recursive: true }, onChange)
  write(`watch src/ (recursive) — ready`)
} catch (err) {
  write(`watch failed: ${err.message}`)
}

// ---- /events WebSocket ----
// Metro's /events broadcast covers every reporter.update() call. Of
// those, we surface the build-status events; everything else (client
// logs in particular) already flows to metro.log so duplicating here
// would just bloat the file. Reconnect on close with backoff because
// Metro restarts during heavy edits, and we want to follow it back.
let socket
let backoff = 500
const MAX_BACKOFF = 8000

function connect() {
  socket = new WebSocket(METRO_EVENTS_URL)
  socket.on('open', () => {
    backoff = 500
    write(`connect ${METRO_EVENTS_URL} — ready`)
  })
  socket.on('message', (data) => {
    let msg
    try {
      msg = JSON.parse(data.toString())
    } catch {
      return
    }
    const t = msg?.type
    switch (t) {
      case 'bundle_build_done':
        write(`bundle_build_done`)
        break
      case 'bundle_build_failed':
        write(`bundle_build_failed`)
        break
      case 'bundling_error': {
        // .error is pretty-printed by createEventsSocketEndpoint, so it's
        // already a string here even though Metro's reporter passed an
        // Error object.
        const e = typeof msg.error === 'string' ? msg.error : JSON.stringify(msg.error)
        write(`bundling_error: ${e.split('\n')[0]}`)
        break
      }
      case 'hmr_client_error': {
        const e = typeof msg.error === 'string' ? msg.error : JSON.stringify(msg.error)
        write(`hmr_client_error: ${e.split('\n')[0]}`)
        break
      }
      case 'transform_cache_reset':
        write(`transform_cache_reset`)
        break
      case 'watcher_status':
        if (msg.status) write(`watcher_status: ${msg.status}`)
        break
      // client_log, bundle_transform_progressed_throttled, dep_graph_loading,
      // initialize_started — intentionally skipped (noise / dup).
    }
  })
  socket.on('close', () => {
    setTimeout(connect, backoff)
    backoff = Math.min(backoff * 2, MAX_BACKOFF)
  })
  socket.on('error', () => {
    // Errors trigger close; close handles reconnect. Don't log per-error
    // because Metro startup churn makes that loud.
  })
}

connect()

function shutdown() {
  try { socket?.close() } catch {}
  try { out.end() } catch {}
  process.exit(0)
}
process.on('SIGTERM', shutdown)
process.on('SIGINT', shutdown)
